起因
起步学node比较晚,所以没看express,直接看了koa,但是多数组件都是通过callback回调来执行,无法直接在koa中使用,所以了解了下koa的实现原理,以便包装现有组件或方法,这里记录下来细节,以作备份,并给大家做个参考吧。
koa依赖co,配合thunkify来执行,虽然co现在已经改成基于promise实现了,但其实原理基本差不多,我这里先分析下co3.x版本。
( 这里需要你对 有一定了解 )
分析
核心是利用generator可以挂起或next(res)来继续执行方法的特性。
这里不考虑异常处理流程,结合适当伪码,给出一个简易的demo便于理解co的控制流程:
常规异步嵌套代码:
fs.readFile('template.html', function (err, template) { // 读取模板 asyncFunc('data.json', function (err, data) { // 获取数据 rep.end(render(template, data)); // 装配并返回 });});
(注:上面的代码仅作示意,并不考虑效率因素。实际上如果模板和数据都是异步的,我们应该同时发起请求,co推荐将异步操作组成数组或对象,它在内部会做平行处理以便优化速度)
我们期望的同步写法:
var tempalte = readFile('template.html');var data = asyncFunc('data.json');rep.end(render(template, data));
为了实现这个功能,我们利用generator的特性,先做一个工具方法迭代generator:
function core(genfunc) { var g = genfunc(); (function next(err, res) { res = g.next(res); if (!res.done) { res.value(next); } })();}
上面这个方法接收一个generatorFunction作为参数,然后实例化它生成迭代器g,next方法不停的反复调用g,并将上一次的返回值作为参数不停的传递下去(有点像promise链式调用)。这里有个地方注意:生成器每次返回的值必须是一个方法,并接受fn回调参数,这样next方法才能作为参数不停的迭代下去(co支持多种数据类型,它自行包装了这些数据类型,最终还是返回方法)。
那么下面就差最后一步了,包装node标准api中的异步方法,以便支持yield:
function thunkify(fn) { return function () { var args = Array.prototype.slice.call(arguments, 0); return function (next) { fn.apply(this, args.concat(next)); } }}// 包装原生APIvar readFile = thunkify(fs.readFile);
现在当调用readFile('path')的时候返回的是一个方法了,也就是yield的返回值,通过在core方法中,我们来调用这个方法,并将next作为参数传递给它,于是整个流程就可以以同步方式执行下去了,我们将刚才的同步代码通过core进行包装,最终得到:
core(function*() { var tempalte = yield readFile('template.html'); var data = yield asyncFunc('data.json'); rep.end(render(template, data));});
结论
这里asyncFunc我用来表示任何异步方法,它应该与readFile类似,返回另一个方法并接受callback回调。标准API通常可以通过thunkify来直接wrap,但如果是自己实现的方法必须要遵守这个约定。
以上代码依赖generator特性,需要安装最新版的node,当前最新版本是0.11.15,通过--harmony参数启动,推荐安装iojs,支持es6多数语法,用来学习了解新语法不错。