Node.js 101: generator
之前介绍了 Promise and async,现在来说说 ECMAScript 6 新加入的 Generator。
In computer science, a generator is a special routine that can be used to control the iteration behaviour of a loop. In fact, all generators are iterators. – Wikipedia
Generator 是一种控制程序迭代的方法,让顺序执行的迭代程序变成异步执行 (此异步和 Node.js 的异步调用有差异) 。在 Node.js 里,它的存在目的就是可以写出更好的控制流。
假设有如下代码:
function loop (arr, cb) {
for (let i = 0, len = arr.length; i < len; i++) {
cb(arr[i])
}
}
如果需要扩展这个代码,增加更多的回调:
function loop (arr, cb) {
for (let i = 0, len = arr.length; i < len; i++) {
for (let j = 0, jlen = cb.length; j < jlen; j++) {
cb[j](arr[i])
}
}
}
loop(arr, [cb1, cb2, cb3, cb4, ...cbN])
换成 generator 就可以这样:
function* loop (arr) {
for (let i = 0, len = arr.length; i < len; i++) {
yield arr[i]
}
}
var ge = loop(['Phil Colson', 9])
var name = ge.next().value
, profile = getProfile(name)
accessLog(name)
var accessLevel = ge.next().value
if (profile.level !== accessLevel) {
throw new Error('You don\'t have privilege!')
}
if (ge.next().done) {
loginDone(profile)
}
从例子可以看到,generator 和一般的函数并没有太大的区别,仅仅需要在 function
后加一个星号即可,而最大差别是,函数是直接调用并返回结果,而 generator 则是调用后,创建并返回一个 constructor 为 GeneratorFunctionPrototype
的对象,然后通过它的 next
方法来让函数执行并获取 yield
语句的值:
function* ge () { yield 1 }
var g = ge()
g.next()
// { value: 1, done: false }
yield
关键字是 generator 的核心,当 next
被调用时,将会执行 generator 内的代码,一旦遇到 yield
时将停止执行,并等待下一个 next
的调用,不停重复,直到所有的代码执行完毕。这赋予了函数异步执行的能力。
function* ge () {
console.log('called')
yield 1
console.log('yield called')
}
var g = ge()
g.next()
// 'called'
// { value: 1, done: false }
g.next()
// 'yield called'
// { value: undefined, done: true }
yield
后面可以是值、对象、函数或表达式,next
调用时返回的 value
默认是 yield
右侧语句的值,那像 var x = yield 1
、fn(yield ARGV)
这样的语句呢?
function* ge () {
var x = yield 10;
console.log(x)
return x + 5
}
var g = ge()
g.next()
// { value: 10, done: false }
g.next(1)
// 1
// { value: 6, done: true }
g = ge()
g.next()
// { value: 10, done: false }
g.next()
// undefined
// { value: NaN, done: true )
第一个 next().value
会是 yield
右侧语句的值,而第二个 next()
的参数将会传递给 yield
左侧的语句;如果 next()
没参数,将是 undefined
。利用这个特性,就可以和 Promise 等组合在一起,实现更方便的功能,这点会在后面介绍。
done
标记是否所有代码已执行完毕。
上面的 loop
经改造成 generator 后,三个 ge.next()
分别返回:
{ value: 'Phil Colson', done: false }
{ value: 9, done: false }
{ value: undefined, done: true }
注意,最后一个 next
被调用时,它的 value
等于 generator function 的返回值,如无 return
,将会是 undefined
:
(function* demo () {
return 'demo'
})().next()
// { value: 'demo', done: true }
Generator 的基础介绍完毕,开始利用 generator 改造 readFiles。
首先把 generator 封装成返回 Promise 的函数:
// https://www.promisejs.org/generators/
function async (makeGenerator) {
return function () {
var generator = makeGenerator.apply(null, arguments);
function handle(result){
if (result.done) {
return Promise.resolve(result.value);
}
return Promise.resolve(result.value).then(function (res){
return handle(generator.next(res));
}, function (err){
return handle(generator.throw(err));
});
}
try {
return handle(generator.next());
} catch (ex) {
return Promise.reject(ex);
}
};
}
async
通过 next()
把 value
给传递到下一个 Promise.resolve
里,再通过 next(res)
把结果传递到 yield
的左边,从而让程序可以像一个同步运行的程序一样。不停重复这几步,直到 next().done === true
为止。
接着要把 fs
的接口封装成 Promise 模式,这里使用了 fs-extra 和 bluebird 这两个库:
const Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra'))
经过 bluebird.promisifyAll
处理的对象的所有函数将被封装成 Promise 模式,封装后的函数一般会在函数名后面增加 Async
,如 fs.readdirAsync
。
经过这两步处理,readFiles
就可以改造成如下所示 (为了缩减代码行数,移除了过滤功能) :
function* readFiles (opts) {
var res = [], files
files = yield fs.readdirAsync(opts.folder);
for (var i = 0, len = files.length; i < len; i++) {
var file = files[i],
fullPath = path.resolve(opts.folder + '/' + file).replace(process.cwd(), '.'),
stat = yield fs.statAsync(fullPath)
if (stat.isDirectory()) {
res = res.concat(
yield async(readFiles)(_.assign({}, opts, { folder: fullPath }))
)
} else if (opts.pattern.test(file)) {
res.push(fullPath)
}
}
return res
}
function Sagase () {
Object.defineProperties(
this,
{
find: {
enumerable: true,
value: function (opts) {
return async(readFiles)(formatOptions(opts))
}
}
}
)
}
经过这样的处理后,readFiles
少了一些多余的嵌套,又保留了该有的异步能力。
Generator 的概念其实就是惰性求值,通过暂停函数执行来让开发者控制程序的控制流,而不是像及早求值那样,语句到那里了就需要立刻求出值。
Generator 并不是用来代替别的控制流的灵丹妙药,它只是一种可能性,让你写出更好的代码的可能性,Koa 的官方例子就很好地解释了这一点:
(via Christoffer Hallas
是不是很像 DOM 的冒泡事件呢?