我的背景是Java。当我第一次学习Node.JS和JavaScript时,我对JavaScript的理解达到了一个相当高的水平,在我试图做一些涉及从数据库中提取数据,将其存储在一个变量中,然后对这些数据进行处理时,遇到了类似的问题。
var songsList = myDatabaseObject.getSongsList();
console.log(songsList); // undefined, why?
这似乎是很简单的事情,对吗?从数据库中获取数据,将其存储在一个变量中,然后将其打印出来,对吗?嗯,JavaScript并不像Java或C++那样是程序化的。简单地说,我们不能指望我们的代码会在JavaScript中 "逐行 "进行,因为JavaScript是另一种类型的野兽。
假设我们要为一个人写一个类。这个人每天早上醒来,按部就班地进行晨练。我们可以在Java中这样表示它。
public class Person {
// Constructor
public Person() {
}
public void wakeUp() {
// do wakeup stuff
}
public void putOnPants() {
// do the putting on of pants
}
public void putOnShirt() {
// do the putting on of a shirt
}
public void putOnShoes() {
// put those shoes on, boi
}
public void goToSchool() {
// now go to school!
}
public static void main(String [] args){
Person person = new Person();
person.wakeUp(); // 1st we do this
person.putOnPants(); // then this
person.putOnShirt(); // then this
person.putOnShoes(); // then this
person.goToSchool(); // and finally this
}
}
而在JavaScript中,我们可能会这样做。
// Constructor
function Person() {
}
Person.prototype.wakeup = function(callback){
// do wakeup stuff
callback();
}
Person.prototype.putOnPants = function(callback){
// do the putting on of pants
callback();
}
Person.prototype.putOnShirt = function(callback){
// do the putting on of a shirt
callback();
}
Person.prototype.putOnShoes = function(callback){
// put those shoes on, boi
callback();
}
Person.prototype.goToSchool = function(callback){
// now go to school!
}
var person = new Person();
person.wakeup(function() {
person.putOnPants(function() {
person.putOnShirt(function() {
person.putOnShoes(function() {
person.goToSchool();
});
});
});
});
现在,我知道你在想什么。"最后那块代码看起来很恶心。"嘿,相信我--当我刚开始的时候,我一直在做这样的事情。在JS世界里,我们把这称为 "回调地狱".如果你不太明白这是怎么一回事,我们一会儿就会说到这一点。
我们之所以需要在JavaScript中使用回调,是因为异步性的概念和JavaScript的非阻塞行为。
同步行为是我们所习惯的!Java的例子就支持这一点。例如,如果你要在早晨起床,你会先起床,然后穿上裤子,再穿上衬衫,然后穿上鞋子,然后去上学!这时,你就会发现你的代码是按顺序执行的。在Java代码中,代码是按顺序逐行执行的。尽管名为**'putOnPants()'的方法可能是20行代码,包含了决定穿哪条裤子的人的程序,但我们可以肯定的是,如果不先穿上衬衫,再穿上鞋子,我们就无法进入goToSchool()**。这是因为Java会阻止下一个方法的调用,直到当前的调用完成。
然而,JavaScript并不阻止方法的调用。因此,如果我们在JavaScript中做同样的事情...
person.wakeUp(); // we want to do this first
person.putOnPants(); // and then this
person.putOnShirt(); // then this
person.putOnShoes(); // then this
person.goToSchool(); // and finally this
...我们可能会注意到,在所有其他方法执行完毕之前,我们就已经执行了person.goToSchool()。所以从理论上讲,你可能在去学校的路上没有穿裤子、衬衫或鞋子!呀。
我们在JavaScript中处理了很多异步行为。这里有几个例子。
- 我的原始问题:我想从数据库中获取一些数据,然后将其打印出来。
- 我们想从服务器上获取一些数据,然后用响应做一些事情。
- 一个用户点击了一个按钮,我们想在他们点击之后做一些事情。
每种情况的共同点是什么?
它们可以在任何时间点上发生(异步)。因此,我们需要某种 "处理事件 "的机制,可以这么说;即:我们需要某种方式来做下一件事。
这就是我们使用回调的原因:在JavaScript中,函数可以被传递为 第一类对象这是一种花哨的说法,即 "它们可以作为常规变量传递",而且这些函数并不总是需要提前声明。
如果你看一下下面的例子,希望这能简化实际发生的情况。
- 在person.wakeUp()的参数里面,我们传入了一个匿名(未命名)的函数。
- 在实际的方法中,这个函数被存储在参数中的回调变量中。
- 在我们做完唤醒的事情后,我们调用那个回调函数,该函数存储着我们传递进去的匿名函数。
- 在第一个匿名函数中,我们调用了person.putOnPants(),并传入了另一个匿名函数。
在Person类的嵌套方法调用中执行匿名函数。
......这个过程不断重复,直到我们到达最后一个匿名函数,它简单地调用person.goToSchool()。我们懒得传入一个参数,但是如果你想在goToSchool()方法之后做一些事情,你只需要像我们之前做的那样做。在参数中传入一个函数,当你做完goToSchool()中的所有事情后,在goToSchool()的最后通过回调调用该函数。
那么,我原来的问题呢,如何解决呢?
// lets say that the function for myDatabaseObject.getSongsList looked like:
Database.prototype.getSongsList = function(callback) {
var db_connection = MySQL.getConn();
var sql = "SELECT SONG_TITLE FROM LIBRARY";
db_connection.executeStatement(sql, function(results, err){
if(!err) {
callback(results); // notice that we can pass in arguments to callback since it's of type [function]!!
}
})
}
// and then we did
var songsList = myDatabaseObject.getSongsList(function(result){
console.log(result); // ['Hit Me Baby One More Time', 'Never Gonna Give You Up']
});
有了!有了这样就可以了,看起来也不坏--结果是干净而简单的。然而,对于我们有许多嵌套回调的问题,这看起来一点也不好。事实上,大多数人会说,让回调嵌套得那么深是不好的做法。这里有另一个解决这个问题的方法,不使用回调。
// Constructor
function Person() {
}
Person.prototype.initMorningRoutine = function(){
console.log("Better get up and get ready");
this.wakeup();
}
Person.prototype.wakeup = function(){
// do wakeup stuff
console.log("Alright, I'm up. Let's put on some pants.");
this.putOnPants();
}
Person.prototype.putOnPants = function(){
// put on pants
console.log("Pants- check. Now what shirt should I wear?");
this.putOnShirt();
}
Person.prototype.putOnShirt = function(){
// do the putting on of a shirt
console.log("Wearing my fav band tee, it's going to be a good day. Now the shoes.");
this.putOnShoes();
}
Person.prototype.putOnShoes = function(){
// put those shoes on, boy
console.log("Docs are on, ready for school.");
this.goToSchool();
}
Person.prototype.goToSchool = function(){
console.log("Bye mom! I'm leaving for school now!");
// now go to school!
}
var person = new Person();
person.initMorningRoutine();
现在,如果你在控制台中运行这个程序,你会看到以下情况
Better get up and get ready
Alright, I'm up. Let's put on some pants.
Pants- check. Now what shirt should I wear?
Wearing my fav band tee, it's going to be a good day. Now the shoes.
Docs are on, ready for school.
Bye mom! I'm leaving for school now!
现在,你已经准备好深入研究JavaScript了,下次遇到回调时,对你来说将是轻而易举的事。