在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