co 是TJ Holowaychuk基于ECMAScript 6 generator 特性开发的一个用于简化异步开发的模块。如果你想尝试使用,请先升级node到0.11版本(不稳定分支)并启用--harmony-generators
选项,或者使用gnode。co的基本使用方法很简单:
var co = require('..');
var fs = require('fs');
function read(file) {
return function(fn){
fs.readFile(file, 'utf8', fn);
}
}
co(function *(){
var a = yield read('.gitignore');
var b = yield read('Makefile');
var c = yield read('package.json');
//输出a的内容
console.log(a);
//输出b的内容
console.log(b);
//输出c的内容
console.log(c);
})()
想弄清楚这段代码里面发生的事情,我们首先需要明白 generator 是怎么玩的(本文只介绍 generator 定义中常用的一部分,详细信息可以参考在线文档)。
首先是创建 generator:
function * gen (){ for (var i = 0; i < arguments.length; i++) { yield arguments[i]; } } var g = gen(1, 2, 3);
通过在
function
关键字后面加入*
我们可以创建一个 generator 构造函数,调用这个构造函数我们就生成了一个 generator 对象。然后是理解yield关键字。首先注意yield只能接收一个参数。generator 对象上有个
next
方法用于迭代,这里迭代的意思就是执行代码到下一个yield关键字,next
方法返回一个对象,对象只有两个属性,value
属性表示 yield 右侧传入的参数,done
属性是布尔值,表示是否所有的 yield 语句均执行完毕。你可以把yield放到无限循环里面,这样done属性永远都不会变为true。比如说function * gen (){ console.log(1); yield 1; console.log(2); yield 2; } var g = gen(); console.log(g.next().value); console.log(g.next().value); console.log(g.next());
第一次调用
next
方法输出两个1,第二次调用next
方法输出两个2,第三次调用next
方法时由于yield已经全部执行完毕,所以返回的对象是{ value: undefined, done: true }
最后,通过向
next
方法传入参数我们可以设定上一次yield调用的返回值,要注意的是yield只能接收一个参数,而且第一次调用next方法传入的参数是无意义的,例如:function * gen (){ var x = yield 1; console.log(x); } var g = gen(); g.next(); //将x赋值为2 g.next(2);
总之,通过generator,我们可以控制函数内部执行到哪个yield,可以知道是否所有yield声明执行完毕,可以对上一次的yield语句赋予一个返回值。
我们来实现一个简易版的co模块,目标是实现上文第一个例子中接受回调函数做为yield参数的API,至于co中支持的其它类型(promise、数组、对象、generator)为了简化程序便于理解,先不做支持。
var slice = Array.prototype.slice;
function co(fn) {
//只支持generator函数做为参数
if (!isGeneratorFunction(fn)) return new Error('only generator supported');
return function(done) {
var ctx = this;
var args = slice.call(arguments);
if (!arguments.length) done = error;
//最后一个参数如果是函数则做为回调函数,其它的传给generator构造函数
else if ('function' == typeof args[args.length - 1]) done = args.pop();
else done = error;
var gen = fn.apply(this, args);
next();
function next(err, res) {
var ret;
if (err) return done(err);
// 多于2个参数时,将除了err的参数整合为数组
if (arguments.length > 2) res = slice.call(arguments, 1);
try {
ret = gen.next(res);
} catch(e) {
return done(e);
}
//所有yield执行完毕, 结束next递归,调用最终回调函数
if(ret.done) return done(null, ret.value);
var called = false;
try {
//ret.value是yield接收的参数,必须为一个接收一个回调函数为参数的函数,而这个回调函数必须把error对象做为第一个参数
ret.value.call(ctx, function() {
if (called) return;
called = true;
//递归调用next方法,这里的arguments第一个总是error,其余为数据
next.apply(ctx, arguments);
})
} catch(e) {
//总是在下次事件轮询时抛错,这样能防止同步异步导致状态不一致的情况
setImmediate(function(){
if (called) return;
called = true;
next(e);
});
}
}
}
}
function error(err) {
if (!err) return;
//下次轮询时抛出错误,正式使用时应该总是传入回调函数,此方法仅仅用于演示
setImmediate(function() {
throw err;
})
}
function isGeneratorFunction(obj) {
return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name;
}
其实质就是递归的调用一个自己构建的next
函数这么的简单,做为练习,你可以考虑考虑如何让co中的yield支持数组做为参数,从而实现并发的支持。
_(完)