koa源码分析系列(四)co-4.0新变化
更新日期:
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的用法发生了改变:
1 2 3 4 5 6 7 8 | 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函数了。
我们看下实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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对象。
例子:
1 2 3 4 5 6 7 8 9 10 | 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的时候。
1 2 | //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里面捕获到这个异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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比较简单如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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也比较容易:
1 2 3 4 | function arrayToPromise(obj) { //直接调用Promise的静态方法包装一个新的promise对象。然后对于每个value调用toPromise进行递归的包装 return Promise.all(obj.map(toPromise, this)); } |
objectToPromise会稍微绕一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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来实现的。不管怎么说,还是值得看一看的。