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

Use io.js and Node.js with Homebrew at OS X

Install [io.js] and [Node.js] with [homebrew] is super easy: brew install iojs node But you make notice this message for io.js formula: This formula is keg-only. iojs conflicts with node (which is currently more established) Because of... Continue →