上一篇分析了co3.x版本的原理,因为co从4.0采用es6的标准promise来实现,简要介绍下:html
( 须要你对 promise 有必定的了解 )node
在这里,yield的返回对象从thunk方法变成promise对象,因为它们都接受方法做为参数,这样generator便能经过这个回调方法来控制,持续迭代下去,这里给个示范代码:es6
function core(genfunc) { var g = genfunc(); var next = function (res) { // 如今res.value是promise,co内部对它其余数据类型包装成promise res.value.then(function (res) { next(gen.next(res)); }, function () { gen.throw(err); }); }; next(gen.next()); }
这里,每次生成器返回的对象变成了promise,而后咱们在promise的resolve方法中递归调用next方法,这样生成器就能够持续迭代下去了. (这里没加终止判断和异常处理)数据库
因为以前的版本中,有不少使用了thunk的包装方法,为了保持兼容,co中对此作了判断,若是res.value不是promise,而是thunk,它会作兼容处理,对此咱们不用修改以前的代码,这里是示范:promise
function thunkToPromise(fn) { return new Promise(function (resolve, reject) { fn(function(err, res) { resolve(res); }); }); }
对于co支持的其余数据类型的封装,我就不介绍了,感兴趣的能够去看co的源码。app
好了,长篇大论铺垫了这么久,该说说koa了,咱们一般使用koa的时候都是经过use添加一个genfunc,因此先看看use作了什么:koa
app.use = function(fn){ this.middleware.push(fn); return this; };
它什么也没作,只是将参数保存起来,而后返回引用,以便支持链式调用,接下来咱们看看koa的启动:oop
app.listen = function(){ var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); };
这个和node的官方http示范很像,没什么要解释的,再看看callback方法:this
app.callback = function(){ var mw = [respond].concat(this.middleware); var gen = compose(mw); var fn = co.wrap(gen); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function(req, res){ res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror); fn.call(ctx).catch(ctx.onerror); } };
这里,在调用co以前,它采用compose方法对以前咱们注册的回调作了一次处理,compose是koa-compose包中的方法,这是源码:code
function compose(middleware){ return function *(next){ var i = middleware.length; var prev = next || noop(); var curr; while (i--) { curr = middleware[i]; prev = curr.call(this, prev); } yield *prev; } }
这里是koa控制流的核心,其实代码很简单,可是须要细心分析下
1.while循环是逆序的,也就是说最后一个genfunc接收的参数是noop, 源码是:
function *noop(){}
2.从倒数第二个genfunc开始,每个方法接受的参数都是紧挨它下一个genfunc的实例,也就是咱们在koa方法中引用的形参: next,这样便能经过在方法中使用yield next来控制流转过程。
3.compose方法返回的也是一个generator,这样co便能迭代它,可是它每次产生的数据并非promise,而是咱们注册的genfunc的实例,因此这里它使用了yield *prev,用来转移迭代对象,这样每次迭代的就是咱们注册是生成器了。
4.若是你看到这里你看懂了,就会有个疑问:为何在咱们注册的生成器中,咱们使用的是yield next,而不是yield *next。其实你怎么写均可以,co内部对res.value作了类型判断,若是是generator,它本身会递归调用co(generator),而co也会返回一个promise。(我写的core方法没有对此进行描述)
这里说明下 yield generator 和yield *generator的区别:前者会将一个generator做为返回值,后者则会将控制交给这个generator,并迭代它,详细介绍能够看 MDN-function *。
写到这里,koa的实现方法和流转控制基本就清楚了,这里作一个总结:
1.在koa注册的genfunc中,能够经过yield next 来控制程序的流转,若是不当心忘记了作这个事情,那么经过上面的代码能够发现,程序会舍弃后面的generator,提早返回。
2.整个控制流实际像一个洋葱,先进的必定会后出,后进入的必定会先出,可是若是咱们故意跳开某一步的next调用,那么这个洋葱就不会被从中间穿过,而是穿过部分外层,而后逆序在穿过这些外层,程序就执行完了,不会进入后面注册的那些genfunc。
3.因为上面的缘由,这里有一个陷阱:若是你使用了相似router之类的组件,一般来讲通常若是匹配了某个指定的path,这个router就会执行,而后它后面的genfunc会被舍弃,它会提早返回。而若是你要配置一些全局的处理,譬如压缩html,关闭数据库链接这类必须等到最后执行的操做,则必须在这些有可能会中断控制流的方法以前注册,而后经过yield next直接转移给下一步便可,否则有可能会被提早返回不被执行。
打完收工,欢迎批评指正。