之前提到过Promise这个概念,本质上是为了避免JS层层异步回调造成的代码难以理解的问题,将异步本身包装在Promise的实现中,从而写出近乎同步版本的异步式代码。不过,Promise只能看作是一个编程的范式,具体怎么做还是需要我们自己操刀。本文介绍一个非常实用的异步处理的库async,它封装的方法极度地简化了常见的一些异步操作(例如:串行有依赖关系的异步操作,或者限制用Node模拟客户端发异步请求的数量等操作)。

async共提供了有二十多个函数,涵盖三大范围:

  • 集合操作:常用对数组或集合中元素的异步操作,如forEach遍历、map映射、filter查找、sortBy排序等
    • each: 如果想对同一个集合中的所有元素都执行同一个异步操作。
    • map: 对集合中的每一个元素,执行某个异步操作,得到结果。所有的结果将汇总到最终的callback里。与each的区别是,each只关心操作不管最后的值,而map关心最后产生的值。
    • filter: 使用异步操作对集合中的元素进行筛选, 需要注意的是,iterator的callback只有一个参数,只能接收true或false。
    • reject: reject跟filter正好相反,当测试为true时则抛弃
    • reduce: 可以让我们给定一个初始值,用它与集合中的每一个元素做运算,最后得到一个值。reduce从左向右来遍历元素,如果想从右向左,可使用reduceRight。
    • detect: 用于取得集合中满足条件的第一个元素。
    • sortBy: 对集合内的元素进行排序,依据每个元素进行某异步操作后产生的值,从小到大排序。
    • some: 当集合中是否有至少一个元素满足条件时,最终callback得到的值为true,否则为false.
    • every: 如果集合里每一个元素都满足条件,则传给最终回调的result为true,否则为false
    • concat: 将多个异步操作的结果合并为一个数组。
  • 流程控制:
    • series: 串行执行,一个函数数组中的每个函数,每一个函数执行完成之后才能执行下一个函数。
    • parallel: 并行执行多个函数,每个函数都是立即执行,不需要等待其它函数先执行。传给最终callback的数组中的数据按照tasks中声明的顺序,而不是执行完成的顺序。
    • whilst: 相当于while,但其中的异步调用将在完成后才会进行下一次循环。
    • doWhilst: 相当于do…while, doWhilst交换了fn,test的参数位置,先执行一次循环,再做test判断。
    • until: until与whilst正好相反,当test为false时循环,与true时跳出。其它特性一致。
    • doUntil: doUntil与doWhilst正好相反,当test为false时循环,与true时跳出。其它特性一致。
    • forever: 无论条件循环执行,如果不出错,callback永远不被执行。
    • waterfall: 按顺序依次执行一组函数。每个函数产生的值,都将传给下一个。
    • compose: 创建一个包括一组异步函数的函数集合,每个函数会消费上一次函数的返回值。把f(),g(),h()异步函数,组合成f(g(h()))的形式,通过callback得到返回值。
    • applyEach: 实现给一数组中每个函数传相同参数,通过callback返回。如果只传第一个参数,将返回一个函数对象,我可以传参调用。
    • queue: 是一个串行的消息队列,通过限制了worker数量,不再一次性全部执行。当worker数量不够用时,新加入的任务将会排队等候,直到有新的worker可用。
    • cargo: 类似于queue,通过限制了worker数量,不再一次性全部执行。不同之处在于,cargo每次会加载满额的任务做为任务单元,只有任务单元中全部执行完成后,才会加载新的任务单元。
    • auto: 用来处理有依赖关系的多个任务的执行。
    • iterator: 将一组函数包装成为一个iterator,初次调用此iterator时,会执行定义中的第一个函数并返回第二个函数以供调用。
    • apply: 可以让我们给一个函数预绑定多个参数并生成一个可直接调用的新函数,简化代码。
    • nextTick: 与nodejs的nextTick一样,再最后调用函数。
    • times: 异步运行,times可以指定调用几次,并把结果合并到数组中返回
    • timesSeries: 与time类似,唯一不同的是同步执行
  • 工具函数
    • memoize: 让某一个函数在内存中缓存它的计算结果。对于相同的参数,只计算一次,下次就直接拿到之前算好的结果。
    • unmemoize: 让已经被缓存的函数,返回不缓存的函数引用。
    • log: 执行某异步函数,并记录它的返回值,日志输出。
    • dir: 与log类似,不同之处在于,会调用浏览器的console.dir()函数,显示为DOM视图。
    • noConflict: 如果之前已经在全局域中定义了async变量,当导入本async.js时,会先把之前的async变量保存起来,然后覆盖它。仅仅用于浏览器端,在nodejs中没用,这里无法演示。

列举完上面各方法的介绍,下面简单说一下我认识的几个重要方法。

由于async封装的是异步任务,所以回调作为参数进行传递是不可缺少的。一般来说,其方法的使用符合一定的规则,如集合操作普遍使用async.fn(collection, iterator(item, callback), done_callback)类的接口,其中done_callback用于错误处理和最终执行完操作的后续回调。下面是一个简单的文件遍历的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
async.each(['A', 'B', 'C'], function (filename, callback) {
fs.readFile(filename, function (err, data) {
if (err) {
return callback(err);
}
// process something to the data
});
}, function (err) {
// do something to handle the error
if (!err) {
console.log('顺利完成所有任务');
}
});

另外,async很人性化地封装了一类方法,使得我们可以:

  1. 将异步任务串行化的方法(*Series),即一个接一个地执行,上面的例子接口名换成async.eachSeries,但是,为了将不同任务串联起来,还需要在异步回调函数中写入callback(null)处理下一个任务。
  2. 控制异步执行的任务数量,还是上面的例子,接口变成async.eachLimit(collection, 2, iterator, callback);,这样每次处理的数量就是2个了。

注意:这里不要把第二个iterator函数中的callback参数与async的方法中第三个回调参数搞混淆,事实上,iterator中的回调参数是对方法第三个参数的包装,使得发生错误的时候能及时得到处理。而对于*Series版本的方法,为了能够将任务依次串联起来,我们需要在iterator内容中显式调用到回调。

async的流程控制方法中,参数为异步任务,为了对异步任务进行流程化处理,其方法接收异步回调函数数组,将它们抽象成一个个地异步任务,如其并行处理方法为:

1
2
3
4
5
6
7
parallel(tasks, [callback])
async.parallel([function (cb) {
setTimeout(); // 异步任务-1
}, function (cb) {
setTimeout(); // 异步任务-2
}], function (err) { // 错误/后续处理函数
});

此外,它还支持类似于whiledo while形式的流程控制,具体可以参考其官方文档说明。

熟悉underscore库的都知道它提供一个函数composite函数,将多个函数绑定到一起返回新的函数,使得调用这个新的函数时,自动会将多个函数依次顺序都执行一遍,前一个函数的返回值当作下一个函数的输入,从而将函数链式地串起来了。当到了异步的情形,这个问题变得有点困难了,主要原因是函数间的不同步执行。async提供了async.compose(fn1, fn2, ...)来达到同样的事情。(注意执行的顺序是从后往前,即fn2 --> fn1)。

另一个我觉得值得一提的方法是queue,前面提到它是一个串行的消息队列,通过限制worker的数量达到控制并发的目的。async.queue可以看作一个工厂方法,它返回的是一个对象。结合代码示例如下,当我们需要用Node模拟客户端请求资源并对同一时段请求数量作一定的限制时候,可以使用如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var q = async.queue(function (url, callback) {
var request = http.get('url', function (res) {
console.log(res.statusCode);
res.on('error', function (err) {
console.log(err);
});
res.on('data', function (chunk) {
});
res.on('end', function () {
callback();
});
});
request.on('error', function (err) {
callback(err);
})
}, 5);
q.push(urls, function (err) {
// 错误处理函数
if (err) {
console.error('出现错误');
}
console.log('请求完成');
});

有没有感觉async的接口都异常地简单呢?熟悉async库后,有种“妈妈再也不用担心我的异步式编程”的感觉啦!


参考链接:

  1. https://github.com/caolan/async
  2. http://blog.fens.me/nodejs-async/