Effective JavaScript Item 62 在异步调用中使用嵌套或者命名的回调函数

在一开始,理解异步程序的调用顺序会有些困难。比如,下面的程序中,starting会先被打印出来,然后才是finished:

downloadAsync("file.txt", function(file) {
    console.log("finished");
});
console.log("starting");

downloadAsync方法在执行之后会立即返回,它只是为下载这个行为注册了一个回调函数而已。 由于JavaScript"运行到完成(Run-to-completion)"的特点,当前执行的代码不会被中断,因此会先打印出starting。直到下载完成之后,JavaScript从事件队列中拿到下载对应的回调函数并执行后,finished才会被输出。

一个便于理解调用顺序的思考方式是:将异步API想成是初始化(Initializing),而不是执行(Performing)了某种行为。用这种思考方式,上面的代码就容易理解了,downloadAsync方法仅仅是在初始化下载这一行为,实际上没有做出实际的下载动作。

那么对于一些依赖于执行顺序的行为,比如在下载发生前,我们首先需要查询下载的目标URL。按照下面的实现方式是不行的:

db.lookupAsync("url", function(url) {
    // ?
});
downloadAsync(url, function(text) { // error: url is not bound
    console.log("contents of " + url + ": " + text);
});

在调用downloadAsync方法的时候,url指向的实际上是undefined。这也很容易理解,lookupAsync方法只是初始化了查询这一行为,实际的动作还没发生,因此作为查询结果的url是不可用的。

最直接的解决方案是使用嵌套(Nesting),通过闭包的特性(关于闭包,请参考Item 11):

db.lookupAsync("url", function(url) {
    downloadAsync(url, function(text) {
        console.log("contents of " + url + ": " + text);
    });
});

现在我们把下载行为放到了查询行为注册的回调函数中,通过闭包的性质,在下载方法中就可以访问到查询方法得到的结果url。

使用这种嵌套的方式来规定异步调用的顺序是很简单的,可是随着调用的数量增加,代码的也会变得难以阅读,就像下面这样:

db.lookupAsync("url", function(url) {
    downloadAsync(url, function(file) {
        downloadAsync("a.txt", function(a) {
            downloadAsync("b.txt", function(b) {
                downloadAsync("c.txt", function(c) {
                    // ...
                });
            });
        });
    });
});

一种防止过度嵌套的方法是将嵌套的回调函数声明为命名函数,然后将需要的数据作为参数传入:

db.lookupAsync("url", downloadURL);

function downloadURL(url) {
    downloadAsync(url, function(text) { // still nested
        showContents(url, text);
    });
}

function showContents(url, text) {
    console.log("contents of " + url + ": " + text);
}

为了让downloadAsync方法的回调函数能够利用url,因此上述代码中还是出现了嵌套的现象。这一嵌套可以借助bind方法进一步消除(关于bind方法,可以参考Item
25
):

db.lookupAsync("url", downloadURL);

function downloadURL(url) {
    downloadAsync(url, showContents.bind(null, url));
}

function showContents(url, text) {
    console.log("contents of " + url + ": " + text);
}

当然使用bind方法确实能够消除过多的嵌套,可是它的问题就是需要声明一些命名函数。当这些函数的数量过多时,也会带来不小的干扰。所以,在使用嵌套和使用bind方法之间通常需要谋求一种平衡,可以将重要的步骤使用命名函数的方式,而其他的步骤还是使用嵌套:

db.lookupAsync("url", function(url) {
    downloadURLAndFiles(url);
});

function downloadURLAndFiles(url) {
    downloadAsync(url, downloadFiles.bind(null, url));
}

function downloadFiles(url, file) {
    downloadAsync("a.txt", function(a) {
        downloadAsync("b.txt", function(b) {
            downloadAsync("c.txt", function(c) {
                // ...
            });
        });
    });
}

对于downloadFiles方法,可以使用抽象程度更高的方法(该方法的实现会在Item 66中进行介绍)。将下载的文件保存到一个数组中:

function downloadFiles(url, file) {
    downloadAllAsync(["a.txt", "b.txt", "c.txt"], function(all) {
        var a = all[0], b = all[1], c = all[2];
        // ...
    });
}

downloadAllAsync方法能够并行地下载多个文件。同时适当的利用嵌套也保证了程序的执行顺序。 在Item 68中,会介绍如何封装程序的执行流程,让流程控制更加简单。

总结

  1. 使用嵌套或者命名回调函数的方式来控制异步行为的执行顺序。
  2. 在嵌套和命名回调函数这两种方式中谋求一种平衡。
  3. 能够并行处理的任务,就不要将它们串行化。
时间: 2024-12-22 18:47:57

Effective JavaScript Item 62 在异步调用中使用嵌套或者命名的回调函数的相关文章

[Effective JavaScript 笔记]第62条:在异步序列中使用嵌套或命名的回调函数

异步程序的操作顺序 61条讲述了异步API如何执行潜在的代价高昂的I/O操作,而不阻塞应用程序继续处理其他输入.理解异步程序的操作顺序刚开始有点混乱.例如,下面的代码会在打印"finished"之前打印"starting",即使这两个动作的程序源文件中以相反的顺序呈现. downloadAsync('file.txt',function(file){ console.log('finished'); }); console.log('starting'); down

Effective JavaScript Item 63 注意异步调用中可能会被忽略的异常

异常处理是异步编程的一个难点. 在同步的代码中,异常可以非常easy地通过try catch语句来完毕: try { f(); g(); h(); } catch (e) { // handle any error that occurred... } 可是在异步代码中,使用一个try代码块将全部可能出现的异常都包含在内是不现实的.实际上,异步API设置不能抛出异常.由于当异常发生时,通常已经没有运行上下文供它抛出了. 全部,在异步API中一般会使用特殊的參数或者错误回调函数来表示异常信息.比方

Effective JavaScript Item 38 在子类构造函数中调用父类构造函数

本系列作为Effective JavaScript的读书笔记. 在一个游戏或者图形模拟的应用中,都会有场景(Scene)这一概念.在一个场景中会包含一个对象集合,这些对象被称为角色(Actor).而每个角色根据其类型会有一个图像用来表示,同时场景也需要保存一个底层图形展示对象的引用,被称为上下文(Context): function Scene(context, width, height, images) { this.context = context; this.width = width

Effective JavaScript Item 13 使用即时调用的函数表达式(IIFE)来创建局部域

本系列作为Effective JavaScript的读书笔记. 所谓的即时调用的函数表达式,这个翻译也许不太准确,它对应的英文原文是Immediately Invoked Function Expression (IIFE).下文也使用IIFE来表达这一概念. 首先看一个程序: function wrapElements(a) { var result = [], i, n; for (i = 0, n = a.length; i < n; i++) { result[i] = function

Effective JavaScript Item 25 使用bind方法来得到一个固定了this指向的方法

本系列作为Effective JavaScript的读书笔记. 当需要将方法抽取出来作为回调函数使用的时候,常常会因为this的指向不明而发生错误,比如: var buffer = { entries: [], add: function(s) { this.entries.push(s); }, concat: function() { return this.entries.join(""); } }; 如果想利用其中的add作为回调函数对一组数据进行添加: var source

HttpContext在多线程异步调用中的使用方案

1.在线程调用中,有时候会碰到操作文件之类的功能.对于开发人员来说,他们并不知道网站会被部署在服务器的那个角落里面,因此根本无法确定真实的物理路径(当然可以使用配置文件来配置物理路径),他们唯一知道的就是文件在项目中的相对路径,为了定位文件路径,一般都会调用HttpContext.Current.Request.MapPath或者HttpContext.Current.Server.MapPath,但是在多线程调用中,HttpContext肯定为null,这时候还调用MapPath结果就是报错.

Effective JavaScript Item 38 调用父类的构造函数在子类的构造函数

作为这一系列Effective JavaScript的读书笔记. 在一个游戏或者图形模拟的应用中.都会有场景(Scene)这一概念.在一个场景中会包括一个对象集合,这些对象被称为角色(Actor). 而每一个角色依据其类型会有一个图像用来表示,同一时候场景也须要保存一个底层图形展示对象的引用,被称为上下文(Context): function Scene(context, width, height, images) { this.context = context; this.width =

Effective JavaScript Item 21 使用apply方法调用函数以传入可变参数列表

本系列作为Effective JavaScript的读书笔记. 下面是一个拥有可变参数列表的方法的典型例子: average(1, 2, 3); // 2 average(1); // 1 average(3, 1, 4, 1, 5, 9, 2, 6, 5); // 4 average(2, 7, 1, 8, 2, 8, 1, 8); // 4.625 而以下则是一个只接受一个数组作为参数的例子: averageOfArray([1, 2, 3]); // 2 averageOfArray([1

Effective JavaScript Item 39 绝不要重用父类型中的属性名

本系列作为Effective JavaScript的读书笔记. 如果需要向Item 38中的Actor对象添加一个ID信息: function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; this.id = ++Actor.nextID; scene.register(this); } Actor.nextID = 0; 同时,也需要向Actor的子类型Alien中添加ID信息: function Alien(