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 1fn(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() 分别返回:

  1. { value: 'Phil Colson', done: false }
  2. { value: 9, done: false }
  3. { 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-extrabluebird 这两个库:

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 的官方例子就很好地解释了这一点:

How generators work in Koa

(via Christoffer Hallas

是不是很像 DOM 的冒泡事件呢?

 
10
Kudos
 
10
Kudos

Now read this

Markdown and CommonMark

CommonMark,最早的名字叫 Standard Markdown,后来迫于 Markdown 原作者 John Gruber 的压力而改名。 虽然这已经是今年九月初的事情了,而且我在当时就已经表达了我的看法,但还是完整说说我的观点。 我对的 Markdown 的看法是初始版本加上 GitHub Flavored Markdown 的代码块语法即可,其他的,算了吧,何必这么复杂?— Chris (@chrisyipw) September 4, 2014... Continue →