koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。
koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。 有下面几种办法体验generator:
- node v0.11 可以使用 (node —harmony)
- 使用gnode来使用,不过据说性能一般
- 使用chrome体验,打开chrome://flags/, 搜索harmony, 启用,重启chrome即可。
核心代码分析
之前写过一篇co的源码分析文章,但是不久之后co就发生了重大变化,就是完全抛弃了thunk风格的函数。全部转用promise。于是,找了个时间我再次看了下源码。简单记录下。
本文假设你已经熟悉了es6里面promise的基本用法。如果不是特别清楚的可以参考下面几篇文章:
- http://purplebamboo.github.io/2015/01/16/promise/
- http://www.w3ctech.com/topic/721
- http://www.cnblogs.com/fsjohnhuang/p/4135149.html
- http://wohugb.gitbooks.io/ecmascript-6/content/docs/promise.html
co4.0全部采用promise来实现。下面我们分析下代码。
首先co的用法发生了改变:
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});
可以看到co还是接受了一个generatorFunction作为参数,实际上参数如果是一个generator对象也是可以的。如果是generatorFunction,co内部会帮你执行生成对应的generator对象。
不同的是co不再返回一个thunk函数,而是返回了一个promise对象。
yield后面推荐的也是promise对象,而不是thunk函数了。
我们看下实现:
function co(gen) {
var ctx = this;
//如果是generatorFunction,就执行 获得对应的generator对象
if (typeof gen === 'function') gen = gen.call(this);
//返回一个promise
return new Promise(function(resolve, reject) {
//初始化入口函数,第一次调用
onFulfilled();
//成功状态下的回调
function onFulfilled(res) {
var ret;
try {
//拿到第一个yield返回的对象值ret
ret = gen.next(res);
} catch (e) {
//出错直接调用reject把promise置为失败状态
return reject(e);
}
//开启调用链
next(ret);
}
function onRejected(err) {
var ret;
try {
//抛出错误,这边使用generator对象throw。这个的好处是可以在co的generatorFunction里面使用try捕获到这个异常。
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
//如果执行完成,直接调用resolve把promise置为成功状态
if (ret.done) return resolve(ret.value);
//把yield的值转换成promise
//支持 promise,generator,generatorFunction,array,object
//toPromise的实现可以先不管,只要知道是转换成promise就行了
var value = toPromise.call(ctx, ret.value);
//成功转换就可以直接给新的promise添加onFulfilled, onRejected。当新的promise状态变成结束态(成功或失败)。就会调用对应的回调。整个next链路就执行下去了。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
//否则说明有错误,调用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) + '"'));
}
});
}
function isPromise(obj) {
return 'function' == typeof obj.then;
}
核心代码主要是onFulfilled与next的实现。
我们先不考虑错误处理看下执行流程。也先不看toPromise的实现。假定我们只是yield一个promise对象。
例子:
co(function* () {
var a = yield Promise.resolve('传给a的值');
var b = yield Promise.resolve('传给b的值');
return b;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});
假设:
Promise.resolve('传给a的值');
生成的叫做promise对象A。Promise.resolve('传给b的值');
生成的叫做promise对象B。
onFulfilled作为入口函数。
- 调用gen.next(res)。这时候代码会执行到
yield Promise.resolve('传给a的值');
然后停住。拿到了返回值‘{value:’promise对象A’,done:false}。 - 然后调用next(ret),传递ret对象。next里面调用promise对象A的then添加操作函数。
- 等promise对象A变成了成功状态,就会再次调用onFulfilled,并且传入resolve的值。
- 于是再次重复1。代码会执行到
yield Promise.resolve('传给b的值');
停住。不同的是这次调用onFulfilled会传递res的值。通过gen.next(res)会把res也就是resolve的值赋值给a。
然后继续这个过程,一直到最后return的时候。
//co包裹的generatorFunction return后 ret.done为true。这个时候就可以resole `Co生成的promise对象`了。
if (ret.done) return resolve(ret.value);
这样整个调用链就执行下去了。可以看到主要是使用promise的then方法添加onfullied操作函数,来实现自动调用gen.next()
。
co的错误处理
co的错误处理主要使用onRejected实现,基本逻辑跟onFulfilled差不多,这边主要说一下gen.throw(err);
的原理。
generator对象的一个特性是可以在generatorFunction外面抛出异常,在generatorFunction里面捕获到这个异常。
function *test(){
try{
yield 'a'
yield 'b'
}catch(e){
console.log('内部捕获:')
console.log(e)
}
}
var g = test()
g.next()
g.throw('外面报错消息')
/*结果
*内部捕获:
*外面报错消息
*
*/
当我们运行gen.next()的时候,会运行到yield ‘a’这一句。这一句正好在内部的try范围内,因此g.throw('外面报错消息')
这个抛出的错误会被捕获到。
如果我们不调用gen.next()或者连续调用三次gen.next()。代码执行不在try的范围,这个时候去gen.throw错误就不会被内部捕获到。
所以co里面用了这个特性,可以让你针对某一个或多个yield加上try,catch代码。 co发现某个内部promise报错就会调用onRejected然后调用gen.throw抛出错误。
如果你不处理错误,co就调用reject(err)传递给包装后的co返回的promise对象。这样你就可以在co(*fn).catch 拿到这个错误。
toPromise的实现
我们看下toPromise的代码:
function toPromise(obj) {
if (!obj) return obj;
//是promise就直接返回
if (isPromise(obj)) return obj;
//如果是generator对象或者generatorFunction就直接用co包一层,最后会返回一个包装好的promise。
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
//如果是thunk函数就调用thunkToPromise转换
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
//是数组就使用arrayToPromise转换
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
//是对象就使用objectToPromise转换
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
主要就是各种判断,把不同类型的yield值转换成一个promise对象。 前面几个都很简单不说了。
thunkToPromise比较简单如下:
function thunkToPromise(fn) {
var ctx = this;
//主要就是新new一个promise对象,在thunk的回调里resolve这个promise对象
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
//错误就调用reject抛出错误
if (err) return reject(err);
//对多个参数的支持
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}
arrayToPromise也比较容易:
function arrayToPromise(obj) {
//直接调用Promise的静态方法包装一个新的promise对象。然后对于每个value调用toPromise进行递归的包装
return Promise.all(obj.map(toPromise, this));
}
objectToPromise会稍微绕一点:
function objectToPromise(obj){
//小技巧,生成一个跟obj一样类型的克隆空对象
var results = new obj.constructor();
//拿到 对象的所有key,返回key的集合数组
var keys = Object.keys(obj);
var promises = [];
//遍历所有值
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
//递归调用
var promise = toPromise.call(this, obj[key]);
//如果转换后是promise对象,就异步的去赋值
if (promise && isPromise(promise)) defer(promise, key);
//如果不能转换,说明是纯粹的值。就直接赋值
else results[key] = obj[key];
}
//监听所有队列里面的promise对象,等所有的promise对象成功了,代表都赋值完成了。就可以调用then,返回结果results了。
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
//先占位
results[key] = undefined;
//把当前promise加入待监听promise数组队列
promises.push(promise.then(function (res) {
//等当前promise变成成功态的时候赋值
results[key] = res;
}));
}
}
objectToPromise的主要思路是循环递归遍历对象的值
- 如果发现是纯粹的值,就直接赋值给结果对象。
- 如果发现是可以转化为promise的就调用defer异步的把值添加到results里面,同时把promise对象放到监听的数组里。
- 这样在最外围只要使用Promise.all去监听这些promise对象。等他们都执行完了代表results已经被正确的赋值。于是再通过then,改变要反回的promise对象的要resolve的值。
结语
整个分析到这就结束了,新版的co代码非常清晰也更加容易理解。不过完全抛弃thunk不知道TJ大神怎么想的。好像目前的koa还是使用的老的co来实现的。不管怎么说,还是值得看一看的。