Koajs让习惯阻塞式代码写法的同学感到很舒服,再也不用盖楼式的callback了,而且也不需要学习Promise的then,catch这些新东西。
但实际上,Koajs这样的写法有点像是语言的语法糖,它只不过把yield又包装成了Promise的链式调用。做这件事儿的库就是co库和compose库,compose库把koajs中的一堆中间件改装成了函数套娃,而co则把这些套娃改装成了Promise链。想要了解Koajs的原理,还真需要先了解一下Promise的基本概念。本文假设你已经了解了Promise的基本知识。
koajs的用法大概是这样的:
var koa = require(‘koa‘)();
koa.use(function* m1(next){
...
yield next;
...
}
koa.use(function* m2(next){
...
yield next;
...
}
koa .listen(3000);
这个koa.listen默认的callback是koa库的application.js中的app.callback,也就是在这个app.callback中,调用了关键的这一句:
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));
这也引出了co库和compose库这两个koajs的核心库。
先说compose库:
koa.use函数关键的代码就是这一句
this.middleware.push(fn);
也就是说这个this.middleware是个中间件的数组。
实际上,compose是把[m1, m2, ...] 改装成了 m1(m2(m3(...))),以方便co库的下一步改装,如下:
function compose(middleware){
return function *(next){
var i = middleware.length;
var prev = next || noop();
var curr;
while (i--) {
curr = middleware[i]; // curr = m3
prev = curr.call(this, prev); // m3.call(this, null) >>> m3() 即 prev=m3()
}
yield *prev; // m3()
}
}
最后的结果相当于返回了一个函数套娃:m1(m2(m3))
这个结果作为co的参数,又发生了什么呢?
co lib:
Actually, co convert yields to promise thens.
// fake codes
function co(gen){
if gen is a function
gen = gen();
if gen is null || gen is not a function
return resolve(gen);
onFulfilled();
// onFulfilled() {
try{
var ret = gen.next();
}catch(){
reject(ret);
}
next();
}
// next() {
if ret.done == true
return resolve(ret);
// toPromise(){
if ret is a general value
return resove(ret)
if ret is a generator // 按深度延展: 处理 yield*
return co(ret)
}
if (isPromise(value))
return value.then(onFulfilled, onRejected); // 按广度延展:处理下一个yield
}
}
这段co代码去除了很多看起来稍稍复杂的细节,有利于捋清楚脉络。
看明白co库和compose库,你就知道,koajs只是给了大家用同步阻塞方式写IO处理的自由,实际上它在背后偷偷的采用了Promise链式调用的方式。
我刚开始了解koajs的时候,最大的疑问也正是这个:为什么一个异步非阻塞的语言可以用阻塞的方式处理异步的IO呢?
现在这个问题有了答案。
2015-11-16 于Evernote