引子
老听人说 koa大法好,这两天我也赶了把时髦:用 n 安上了node 0.11.12,下了个koa开启harmony模式试水。在一系列文档和贴子的教育下,大概认识到:
- koa 是TJ 大神主导的新一代Web框架
- koa 的中间件基于ES6的生成器函数(function *)形式
- koa的核心流程库是 co,它能很好的解决Pyramid of Doom问题
在接触 Node.js 前,由于有过 Python编程的经验,我对生成器是个什么东西已经是很清楚了。我真正感兴趣的是:它是怎么被用来优化回调嵌套的。
“为何这么屌”
在 KOA框架为何这么屌 一文中有这么一段(片断1):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var fs = require('fs'); var app = require('koa')(); var readFile = function(dir) { return function(fn) { fs.readFile(dir, fn); } } app.use(function* () { var arr = yield ['1.txt', '2.txt', '3.txt'].map(function(path) { return readFile(path); }); this.body = arr.join(','); }) app.listen(8000); |
这段代码很好的演示了koa是如何利用生成器函数(function*(),函数的constructor.name === ‘GeneratorFunction’ )来串行化异步回调的,它的执行流程:
function*(){...}
被做为生成器函数push到了koa的中间件队列中- koa使用co框架,对这个生成器函数进行调用执行;执行生成器函数并不立即执行函数体,而是生成生成器(generator)实例——同时,生成器可视为遵循迭代器协议的实例,每次调用迭代器的next(),都会返回一个
{ value: obj, done:true/false }
对象:value是执行结果值,done指示迭代是否完成 - 调用生成器实例的next()方法将启动函数体内部的执行流,直到出现yield时被挂起,那么下一次的next()将会给yield返回执行的结果值,并挂起在下一个yield出现处;可以理解为,yield总是返回上一次next()的结果值,如果next()有参数,yield将返回这个参数值(异步回调有机会注入执行结果)
- 观察上面代码片段我们注意到,readFile的回调处理函数fn代码中并未提供,那么koa或者说co是怎么处理这个回调函数的呢?在 koa(0.13.0)自带的co源码(line:84)中,可以看到如下片断(片断2):
1234567891011121314151617181920// normalizeret.value = toThunk(ret.value, ctx);// runif ('function' == typeof ret.value) {var called = false;try {ret.value.call(ctx, function(){if (called) return;called = true;next.apply(ctx, arguments);});} catch (e) {setImmediate(function(){if (called) return;called = true;next(e);});}return;}
toThunk 会根据 yield返回的表达式转换成Thunk函数:一个关于回调的高阶函数(片断3):
123456789101112131415161718function toThunk(obj, ctx) {if (isGeneratorFunction(obj)) {return co(obj.call(ctx));}if (isGenerator(obj)) {return co(obj);}if (isPromise(obj)) {return promiseToThunk(obj);}if ('function' == typeof obj) {return obj;}if (isObject(obj) || Array.isArray(obj)) {return objectToThunk.call(ctx, obj);}return obj;} - 在片断1 中,生成器函数首先返回的是一个生成器;然后,yield 结合map会返回三个function对象,即高阶函数readFile返回的function:
123function(fn) {fs.readFile(dir, fn);} - 片断2 根据 片断3 返回的类型,对 function 进行了call调用,并提供了回调函数:将arguments通过
next.apply(ctx, arguments);
巧妙的进行传递。如前所述,next()如果提供了参数,yield得到的结果值就是这个参数,回调结果由此而来。
到底是谁屌
如果看官看完上面那几段还没晕,那当然是您最屌:) ——我的表述能力确实不足以很清晰的道出框架的玄机,但在我看来,真正屌的是ES6 Generator机制本身。
暂时放下 co 框架,把 片断1 稍加改造(片断4):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var fs = require('fs'); var path = require('path'); var readFile = function (dir) { return function (fn) { fs.readFile(dir, {encoding: 'utf8', flag: 'r'}, fn); }; }; function *readFileGeneratorFunction(path, cb){ console.log(yield readFile(path)(cb)); } var readFileIterator = readFileGeneratorFunction('testDate.js', callback); function callback(err, data){ readFileIterator.next(data); } readFileIterator.next(); |
用意很明显:
- 这个readFileGeneratorFunction就是个生成器函数,执行它返回一个生成器(迭代器)
- 高阶函数返回的function,在生成器函数执行时指定了回调
- next触发执行
- 回调完成时,next(data)携带结果值触发yield
问题也很明显,业务代码(GeneratorFunction中的yield) 需要前置于流程控制(callback),这不科学。抽象一下,可以提供一个生成器函数的执行函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var slice = Array.prototype.slice; function run(generatorFunction) { try { var generatorItr = generatorFunction(callback); function callback(err, res) { if(err) generatorItr.throw(err); else { var args = slice.call(arguments, 1); res = args.length > 1 ? args : res; generatorItr.next(res); } } generatorItr.next(); } catch (e){ console.log(e.message | "I'm died."); } } |
测试一下:
1 2 3 4 5 6 |
run(function* rfGenFunc(cb) { console.log('first'); console.log(yield readFile('1.txt')(cb)); console.log('second'); console.log(yield readFile('2.txt')(cb)); }); |
小结
本文仅对Generator的next()应用进行了简单的描述(其实它还有更多内容如:throw/send/close),抛砖引玉罢了。至于生成器特性,目前仍处于 ECMAScript 6 规范草案中,如MDN所言:请谨慎使用 🙂
打赏作者
您的支持将激励我继续创作!
toThunk 好像是说 原本的
function (arg1, fn) {
do sth…
fn();
}
就会变成
function (arg1) {
return function(fn) {
fn()
}
}
好像可以这样理解
恩,就是一个高阶函数
有个地方似乎写错了:
「可以理解为,yield总是返回上一次next()的结果值,如果next()有参数,yield将返回这个参数值(异步回调有机会注入执行结果)」中的「yield总是返回上一次next()的结果值」。
yield并不会返回next()的结果,只会返回next()的参数(无参则undefined)
多谢指教
alert(‘跨站攻击鸟’)