KOA的流转控制
上一篇分析了co3.x版本的原理,由于co从4.0采用es6的标准promise来实现,简要介绍下:
( 需要你对 promise 有一定的了解 )
在这里,yield的返回对象从thunk方法变成promise对象,由于它们都接受方法作为参数,这样generator便能通过这个回调方法来控制,持续迭代下去,这里给个示范代码:
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,它会做兼容处理,对此我们不用修改之前的代码,这里是示范:
function thunkToPromise(fn) {
return new Promise(function (resolve, reject) {
fn(function(err, res) {
resolve(res);
});
});
}
对于co支持的其他数据类型的封装,我就不介绍了,感兴趣的可以去看co的源码。
好了,长篇大论铺垫了这么久,该说说koa了,我们通常使用koa的时候都是通过use添加一个genfunc,所以先看看use做了什么:
app.use = function(fn){
this.middleware.push(fn);
return this;
};
它什么也没做,只是将参数保存起来,然后返回引用,以便支持链式调用,接下来我们看看koa的启动:
app.listen = function(){
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
这个和node的官方http示范很像,没什么要解释的,再看看callback方法:
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包中的方法,这是源码:
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直接转移给下一步即可,不然有可能会被提前返回不被执行。
打完收工,欢迎批评指正。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。