控制回调流程
当异步操作的时候,通常都是通过回调函数来进行反馈。
如果你需要进行一些IO操作,那么基本都需要指定一个在操作结束的时候调用的回调函数,如果涉及大量IO操作,要把这些回调管理好就很麻烦,也许会导致一些被称为“回调汤”或者“飞去来器效应”的模式。
一、飞去来器效应
这个效应发生在一组回调按顺序执行时——当一个IO操作结束,另一个操作开始。这个效应的名称源于,如果代码中回调过多,就会多层嵌套,导致代码长得像一个飞行器,也是我们所说的:回调地狱。
二、怎么避免飞去来器效应
可以通过给函数命令,并且在同一作用域声明它来避免飞去来器效应。
比如你需要执行A,B,C三个操作,那么你可以把ABC三个操作分别提出来,进行函数封装,在需要执行这一系列代码逻辑的时候调用入口函数A,等到A执行完毕之后,在A之中调用B,等B执行完毕,在B中调用C,这样就可以将每一步的逻辑写到同一个作用域下,从而不必陷入回调地狱,导致多层嵌套。
但是这样会有几个显著问题:
- 每一步都需要处理错误,导致大量冗余代码
- 每一步都需要知道下一步执行的函数名,耦合度高。
三、使用 ASYNC 流程控制库
async模块可以很方便简化异步模式:
可以用命令安装这个库:
npm install async
async模块有一组辅助函数,这些辅助函数可以进行一步迭代以及流程控制。
3.1 串行执行
使用async.series
将两个异步操作串接起来:
const async = require('async')
function done(err, results){
if(err) throw err;
console.log(results);
}
async.series([
function(next){
异步操作
next(err, ... );
},
function(next){
异步操作
next(err, ... );
}
], done)
所有函数执行完毕之后,会调用done
函数,此时done函数带有每个回调函数的异步结果。
3.2 并行执行
前面的例子中,每个回调函数逐个被执行,但是也可以让他们并行执行,此时只需要将series
改成parallel
即可。
3.3 连续传递
同样还可以执行一串函数,其中下一个函数的执行取决于前一个回调函数的结果:
function done(err, res, body){
if(err) throw err;
console.log(body);
}
async.waterfall([
function(next){
request.post({url:"...", body: "3"}, next);
},
function(res,body,next){
request.post({url:"...", body: body}, next};
}
], done)
每个回调函数都会得到最后一个操作(done)的回调参数(除了err),而如果出现错误,则会调用最后一个回调函数(done)。
3.4 排队
如果需要进行一些重复性的异步操作并想控制并发数——在给定时刻同时存在的正在进行的作业的最大数量——就可以使用saync.queue
函数。
该函数创建一个队列,该队列基于一个函数处理其中的元素,队列的客户端可以将作业和并发作业的最大数量传入这个函数,并按顺序执行。
const maxSize = 5;
const work = function(task, callback){
request.post(options, (err, res, body) => {
callback(err, body && JSON.parse(body));
})
}
const queue = saync.queue(work, maxSize);
[1,2,3,4,5,6,7,8,9,10].forEach(function(i) {
queue.push(i, function(){
...
})
})
上述代码构建了一个 work 函数,用这个函数来接收和处理任务。本例中的任务就是一个数字。
queue
函数基于work
函数创建了一个队列,并定义了最大并发数5。
使用队列的优点就是限制了并发量,可以用它来限制资源的流失。
可以通过给队列的 saturated 属性传入一个函数,来在队列并发达到上限的时候调用该函数:
queue.saturated = function(){
console.log("full!");
}
同样,也可以给 empty 属性绑定函数,在队列中最后一项被分配给一个作业后调用。
当返回最后一项作业被返回,会调用 drain 属性上绑定的函数。
3.5 迭代
JS有一些函数可以用来在集合上面进行同步迭代,例如:
[1,2,3,4].forEach(function(i){
...
})
如果不使用同步函数,就必须进行一些IO操作。
使用异步函数async.forEach()
在对象集合上进行迭代:
function done(err){
...
}
const collection = [1,2,3,4];
function iterator(value , callback) {
request.post({
...
}, (err, res, body) => {
...
callback();
})
}
async.forEach(collection, iterator, done);
iterator 函数利用集合中的元素进行必要的IO操作,并且在IO操作结束之后会调用回调函数。当在所有元素上完成了异步迭代时,就会调用最后一个回调函数(done)。
async.forEach函数会针对每个元素并行调用迭代器,如果希望同步进行,可以使用async.forEachSeries
函数。
3.6 映射
JS还提供了一个函数array.map()
,用来同步迭代所有元素,不过迭代是异步进行的,只需要把forEach换成map即可,而且在获得所有结果之后可以按正确顺序返回给回调函数done。
3.7 规约
JS还有一个array.reduce
函数,它将一个初始值和一个迭代器作为参数,并返回一个新值,然后将这个新值和新元素一起传入下一次迭代。
例如求和:
const collection = [1,2,3,4];
function iterator(sum, elem){
return sum + elem;
}
const sum = collection.reduce(iterator,0);
async.reduce
函数提供了相同的语义,但是他是异步进行的:
async.reduce(collection, 0, iterator, done);
3.8 过滤
可以基于一个过滤函数进行过滤:
const isEven(item){
return item % 2 === 0;
}
const result = collection.filter(isEven);
async.filter
可以异步执行相似操作:
async.filter(collection, filter, done);
3.9 检测
如果你想当满足某个条件的时候停止迭代,可以使用detect
。