如何轻松地理解JavaScript中的回调?

92 阅读5分钟

我的背景是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中,函数可以被传递为 第一类对象这是一种花哨的说法,即 "它们可以作为常规变量传递",而且这些函数并不总是需要提前声明。

如果你看一下下面的例子,希望这能简化实际发生的情况。

  1. person.wakeUp()的参数里面,我们传入了一个匿名(未命名)的函数。
  2. 在实际的方法中,这个函数被存储在参数中的回调变量中。
  3. 在我们做完唤醒的事情后,我们调用那个回调函数,该函数存储着我们传递进去的匿名函数。
  4. 在第一个匿名函数中,我们调用了person.putOnPants(),并传入了另一个匿名函数。

image.png

在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了,下次遇到回调时,对你来说将是轻而易举的事。