在js或者node编程中,由于异步的频繁和广度使用,使得回调和嵌套的深度导致编程的体验遇到一些挑战,如果写出优雅和好看的代码,本文主要针对异步编程的主流方案做一些总结
1、事件发布/订阅模式
事件监听器模式是一种广泛用于异步编程的模式, 是回调函数的事件化,又称发布/订阅模式, node自身提供events模块,是该模式的一个简单实现。
EventPorxy
2、promise/deferrd模式
在2009年被Kris Zyp抽象为一个提议草案,发布在CommonJS规范中, 目前,CommonJS草案中已经包括Promise/A、Promise/B、Promise/D这些异步模型。由于Promise/A较为常用也较为简单,只需要具备then()方法即可。所以先介绍一些改模式。
一般来说,then()的方法定义如下:
then(fulfilledHandler, errorHandler, progressHandler)
Promises/A
通过继承Node的events模块,我们可以实现一个简单Promise模块。
promise部分
var Promise = function() {
EventEmitter.call(this);
}
util.inherits(Promise, EventEmitter); // util是node自带的工具类 Promise.prototype.then = function(fulfilledHandler, errorHandler, progressHandler) {
if(typeof fulfilledHandler === "function") {
this.once('success', fulfilledHandler);
}
if(typeof errorHandler === "function") {
this.once('error', errorHandler);
}
if(typeof progressHandler === "function") {
this.on('progress', progressHandler);
}
return this;
}
deferred部分
var Deferred = function() {
this.state = 'unfulfilled';
this.promise = new Promise();
} Deferred.prototype.resolve = function(obj) {
this.state = 'fulfilled';
this.promise.emit('success', obj);
} Deferred.prototype.reject = function(obj) {
this.state = 'failed';
this.promise.emit('error', obj);
} Deferred.prototype.progress = function(obj) {
this.promise.emit('progress', obj);
}
使用
function readFile(file, encoding) {
var deferred = new Deferred();
fs.readFile(file, encoding, deferred.resolve);
return deferred.promise;
} readFile('/test.txt', 'utf-8').then(function(data) {
...
}, function(err) {
...
});
以上是promise/deferred模式的简单实现,状态转换图可以描述如下:
promise模式比发布/订阅模式略为优雅, 但还不能满足很多场景的实际需求,比如一组纯异步的API为了协同完成一串事情。
Promises/A+
规范将之前 Promises/A 规范的建议明确为了行为标准。其扩展了原规范以覆盖一些约定俗成的行为,以及省略掉一些仅在特定情况下存在的或者有问题的部分
详见:中文版:http://www.ituring.com.cn/article/66566, 英文版:https://promisesaplus.com/
3、流程控制库
尾触发与next
尾触发目前应用最多的地方是Connect的中间件, 中间件处理网络请求时,可以向面向切面编程一样进行过滤、验证、日志等功能,最简单的中间件如下:
function(req, res, next) {
//中间件
}
每个中间件传递请求对象、响应对象和尾触发函数,通过队列形成一个处理流,如下:
看一个例子
app.use(function(req, res, next) {
setTimeout(function() {
next();
}, 0)
}, function(req, res, next) {
setTimeout(function() {
next();
}, 0);
});
从这个实例中可以简单猜到尾触发的实现原理了,简单讲就是通过调用use维护一个队列, 调用next的时候出队并执行,依次循环。
async
目前最知名的流程控制模块,async模块提供了20多个方法用于处理异步的多种写作模式, 如:
1、异步的串行执行
async.series([
function(callback){
// do some stuff ...
callback(null, 'one');
},
function(callback){
// do some more stuff ...
callback(null, 'two');
}
],
// optional callback
function(err, results){
// results is now equal to ['one', 'two']
}); // an example using an object instead of an array
async.series({
one: function(callback){
setTimeout(function(){
callback(null, 1);
}, 200);
},
two: function(callback){
setTimeout(function(){
callback(null, 2);
}, 100);
}
},
function(err, results) {
// results is now equal to: {one: 1, two: 2}
});
异常处理原则是一遇到异常,即结束所有调用,并将异常传递给最终回调函数的第一个参数
2、异步的并行执行
// an example using an object instead of an array
async.parallel({
one: function(callback){
setTimeout(function(){
callback(null, 1);
}, 200);
},
two: function(callback){
setTimeout(function(){
callback(null, 2);
}, 100);
}
},
function(err, results) {
// results is now equals to: {one: 1, two: 2}
});
与EventProxy基于事件发布和订阅模式的不同在于回调函数的使用上, async回调函数由async封装后传入, 而EventProxy则通过done(), fail()方法来生成新的回调函数, 实现方式都是高阶函数的应用。
3、异步调用的依赖处理
async.waterfall([
function(callback){
callback(null, 'one', 'two');
},
function(arg1, arg2, callback){
// arg1 now equals 'one' and arg2 now equals 'two'
callback(null, 'three');
},
function(arg1, callback){
// arg1 now equals 'three'
callback(null, 'done');
}
], function (err, result) {
// result now equals 'done'
});
4、自动依赖处理
async.auto({
get_data: function(callback){
console.log('in get_data');
// async code to get some data
callback(null, 'data', 'converted to array');
},
make_folder: function(callback){
console.log('in make_folder');
// async code to create a directory to store a file in
// this is run at the same time as getting the data
callback(null, 'folder');
},
write_file: ['get_data', 'make_folder', function(callback, results){
console.log('in write_file', JSON.stringify(results));
// once there is some data and the directory exists,
// write the data to a file in the directory
callback(null, 'filename');
}],
email_link: ['write_file', function(callback, results){
console.log('in email_link', JSON.stringify(results));
// once the file is written let's email a link to it...
// results.write_file contains the filename returned by write_file.
callback(null, {'file':results.write_file, 'email':'user@example.com'});
}]
}, function(err, results) {
console.log('err = ', err);
console.log('results = ', results);
});
在现实的业务环境中,具有很多复杂的依赖关系, 并且同步和异步也不确定,为此auto方法能根据依赖关系自动分析执行。
Step
轻量的async, 在API暴露上也具备一致性, 因为只有一个接口Step。
在异步处理上有一些不同, Step一旦产生异常,会将异做为下一个方法的第一个参数传入
var s = require('step');
s(
function readSelf() {
fs.readFile(__filename, this);
},
function(err, content) {
//并行执行任务
fs.readFile(__filename, this.parallel());
fs.readFile(__filename, this.parallel());
},
function() {
//任务分组保存结果
var group = this.group();
console.log(arguments);
fs.readFile(__filename, group());
fs.readFile(__filename, group());
},
function () {
console.log(arguments);
}
)
Wind
待补充
总结
对比几种方案的区别:事件发布/订阅模式相对是一种原始的方式,Promise/Deferred模式贡献了一个非常不错的异步任务模型的抽象,重头在于封装异步的调用部分, 而流程控制库则要灵活很多。
除了async、step、EventProxy、wind等方案外,还有一类通过源代码编译的方案来实现流程控制的简化, streamline是一个典型的例子。
参考
《深入浅出nodejs》第四章
https://promisesaplus.com/
https://github.com/caolan/async/
https://github.com/creationix/step
http://www.ituring.com.cn/article/66566