前言
大名鼎鼎的co正是TJ大神基于ES6的一些新特性开发的异步流程控制库,基于它所开发的koa更是被视为未来主流的web框架。之前在论坛也看了不少大神们关于co源码的分析,不过co在升级为4.X版本时,代码进行了一次颇有规模的重构,从先前的基于thunkify函数,改变成了现在的基于Promise,并且可能在未来版本移除对thunkify函数的支持。正好小弟最近也在看TJ大神的源码,也来分享一下co(4.X)的实现思路以及源码注解。
进入正题
以下是两段co(4.X)的经典使用示例:
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});
co(function *(){
// resolve multiple promises in parallel
var a = Promise.resolve(1);
var b = Promise.resolve(2);
var c = Promise.resolve(3);
var res = yield [a, b, c];
console.log(res);
// => [1, 2, 3]
}).catch(onerror);
再结合co的文档,我们可以把co的主要功能分为:
- 异步流程控制,依次执行generator函数内的每个位于yield后的Promise对象,并在Promise的状态改变后,把其将要传递给reslove函数的结果或传递给reject函数的错误返回出来,可供外部来进行传递值等操作,这些Promise是串行执行的。
- 若yield后是Promise对象的数组或属性值是Promise对象的对象,则返回出结构相同的Promise执行结果数组/对象,并且这些Promise是并行执行的。
- co自身的返回值也是一个Promise对象,可供继续使用。
好了,我们要实现以上这几个功能,其实就是解决以下几个问题:
1,确保每一个yield动作的串行执行,并正确的返回异步结果。 2,若在yield后的是Promise数组或属性值为Promise对象的对象,则并行执行这些Promise。 3,检查每一个yield后的对象是否符合要求,若不,则尝试进行转换。 4,最后整体返回一个Promise。
我们来逐个击破。
解决1,4
以下是co(4.X)源码中的解决1,4的方案:
function co(gen) {
var ctx = this;
/**
* 我们看到,co函数整个的返回值便是一个Promise实例,包装了
* 传递的generator函数内所有Promise的执行,
* 我们暂且称它为"外壳Promise"吧,而在传递进来的generator函数内的所有Promise我们都先称为“内部Promise”,
* 用以区分。
*/
return new Promise(function(resolve, reject) {
//判断传递给co的参数,若是一个generator函数,则执行之,得到一个generator对象
if (typeof gen === 'function') gen = gen.call(ctx);
//此时gen对象还不是一个generator对象的话,
//则调用“外壳Promise”的resolve,直接结束整个“外壳Promise”,把参数值作为结果传出
if (!gen || typeof gen.next !== 'function') return resolve(gen);
//入口,这个函数的详细分析请往下看两行。。
onFulfilled();
//这个onFulfilled函数主要有两个用途:
//第一个用途就是上面的入口函数的功能,将generator执行到第一个yield处开启第一个异步调用
//第二个用途便是当作所有”内部Promise“的resolve方法,处理异步结果,并继续调用下一个Promise
function onFulfilled(res) {
var ret;
try {
//当作为”内部Promise“的resolve方法时,res参数自然便是本次Promise的执行结果了
//利用generator函数的特性,调用next方法时的参数,会当做yield的返回值,这样我们就做到了将异步的结果返回出来
ret = gen.next(res);
} catch (e) {
//若报错,则直接调用"外壳Promise"的reject方法,直接结束整个“外壳Promise”,把错误对象作为结果传出
return reject(e);
}
//将generator.next的执行结果传入next函数,实现串行调用。关于next函数的分析也请往下看。。
next(ret);
}
//这个onRejected函数的用途自然便是当作"内部Promise"的reject方法啦
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
//这个next函数便是用来执行串行调用
function next(ret) {
//如果“内部Promise”,全部执行完毕,done的值便已经是generator函数中的return出的值了,
//把这个结果传递给“外壳Promise”的resolve函数,暴露给处理整个co结果的后续调用
if (ret.done) return resolve(ret.value);
//若“内部Promise”尚未执行完毕,那么确保ret.value是一个Promise对象,并进而调用它
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
好,到这儿我们已经把co4.X的核心串行调用的实现过程给搞定了。整个的执行流程可以归结为:
- 进入“外壳Promise”
- 通过入口onFulfilled(),取得第一个Promise对象
- 将每一个“内部Promise”通过then(onFulfilled, onRejected)开始执行,并将resolve函数都封装为onFulfilled(),reject函数都封装为onRejected()
- 通过在onFulfilled()或onRejected()内调用next方法,实现在一个异步调用结果返回后继续执行下一个,通过将结果作为参数传递给next方法,实现将异步结果返回给外部,依次往复。
- 所有“内部Promise”执行完毕,将最后结果暴露给“外壳Promise”的处理函数,结束
解决2,3
在TJ大神的源码中,2其实伴随在3(即上面代码中的toPromise方法)中解决的,我们来看一下:
/**
* 主要任务便是将传入的参数对象转换为Promise对象
*/
function toPromise(obj) {
//确保obj有意义
if (!obj) return obj;
//如果已经是Promise对象,则直接把obj返回出去
if (isPromise(obj)) return obj;
//若是generator函数或现成的generator对象,则直接把obj作为参数传入co函数,并把这个co函数
//返回出来的"外壳Promise"作为return出来的Promise
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
//若obj是函数,则直接视为符合thunk规范的函数(thunk函数是啥这里就不细说了哈。。),直接转换
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
//若是Promise数组,则调用arrayToPromise方法
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
//若是属性值是Promise对象的对象,则调用objectToPromise方法
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
5个if其实就是一堆的判断并执行响应转换咯(废话。。),看来已经解决了问题3,问题2的精髓自然就是arrayToPromise和objectToPromise,好,我们来攻克这最后一道壁垒:
先是arrayToPromise:
/**
* 利用ES6规范中的Promise.all方法,充当全部结果的返回者
* 利用Array.map方法,实现了并行操作,分别对数组中的每一个元素递归执行toPromise方法,把这些子Promise接着
* 返回co中来获取执行结果,最后等待这些子Promise全部得到结果后,Promise.all执行成功,
* 返回执行结果数组
* 这实现,请收下我的膝盖。。。
*/
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
接着是objectToPromise:
/**
* 与数组略有不同,利用for循环来实现并行的异步调用,
* Promise.all()仅充当一个类计数器,并返回最终结果
*/
function objectToPromise(obj){
//results是将用于返回的对象,使用和obj相同的构造函数
var results = new obj.constructor();
//Object.keys方法用于返回对象的所有的属性名
var keys = Object.keys(obj);
//寄存所有对象属性的Promise的数组
var promises = [];
//利用for循环来实现异步
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
//确保obj[key]为Promise对象,然后调用defer推入promises数组等待执行,否则直接将结果返回给result[key]
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
//传入的是promise.then()返回的空Promise,所以此处Promise.all仅充当一个计数器,确保所有异步操作的resolve操作中对results对象的属性都赋值完毕后,返回最终的results对象
return Promise.all(promises).then(function () {
return results;
});
//执行异步操作,并在操作结果赋值给results[key]
function defer(promise, key) {
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
好了,到这我们已经把co(4.X)的核心源码实现都搞定,它的庐山真面目就是这样啦~
如有任何不正确之处,欢迎指出^ ^
参考
mark
mark!!
用yield*
可以很容易地处理数组
只考虑 yield Promise的情况,源码还是很简单的…
// http://magicdawn.ml/2015/01/10/generator-and-promise/
function co(genFn) {
var gen = genFn(); // generator
return new Promise(function(reslove, reject) {
next()
function next(lastPromiseVal) { // 上一个promise的val
try {
var ret = gen.next(lastPromiseVal)
}
catch (e) {
reject(e)
}
// yield 返回了一个 ret.value = promise
var promise = ret.value;
if (ret.done) {
reslove(promise);
}
else {
promise.then(function(val){
next(val)
});
}
}
});
}
@think2011 : )
@magicdawn 那是的确…
感谢!
mark