本文翻译自 @LUIS VEGA 的文章 Understanding Node Generators

 

node(a) 当前的稳定版本是 v0.10.29,尚未支持 generators。要使用 generator,需要安装 0.11.9 版,并在启动 node 服务器是指定 –harmony 标志,即 node --harmony

 

什么是 generator?

Generator 函数是一种特殊的函数,只需要在其中使用 yield,就可以暂停其执行过程。调用 generator 函数时,会得到一个 generator 对象的返回值(b)。要想运行在 yield 位置之前的函数体,需要调用 generator 对象的 next 方法。定义 Generator 函数的方法就是在函数名之前加一个星号(*),例如:

function *generatorFunctionName() { … };

 

在这之前,你可能已经阅读了大量资料——就是还没有完全理解。没关系,用例子来解释总是最好的。在看了下面的例子之后,我建议你再读一遍上面那段。

// 在 REPL 中执行代码,就可以看到返回值了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<span class="kwrd">function</span> *generatorFunction() {
  console.log(<span class="str">"A"</span>);

  yield <span class="str">"First Pause!"</span>;

  console.log(<span class="str">"B"</span>);    

  yield <span class="str">"Second Pause"</span>;

  console.log(<span class="str">"C"</span>);
};

<span class="kwrd">var</span> generatorObject = generatorFunction();

generatorObject.next();
<span class="rem">// => A</span>
<span class="rem">// => { value: 'First Pause!', done: false }</span>

generatorObject.next();
<span class="rem">// => B</span>
<span class="rem">// => { value: 'Second Pause!', done: false }</span>

generatorObject.next();
<span class="rem">// => C</span>
<span class="rem">// => { value: undefined, done: true }</span>

 

可以看到,每次调用 next,执行过程就会暂停。调用 next 总是返回一个简单的 JavaScript 对象,它有两个属性:valuedonevalue 包含你 yeild 后面表达的值;done 则包含一个 bool 值,如果还有更多的 yield 需要执行,则为 false©。(我自己也经常把这些返回值弄混淆,所以为了便于记忆,请重复提醒自己 “只有用 yield 才产生值”)

考虑 yield 就是一种类似于 return 的语句。它之所以类似 return,是因为在 yield 后面的表达式的值会在外部调用 next 时返回给调用方。 不过与 return 不同的是,函数的运行并没有终止,你可以继续执行函数。

如果再次调用 nextgeneratorObject 就会执行到第二个(也是最后一个)yield,然后重复第一次调用 next 的过程。

最后,如果第三次调用 nextgeneratorObject 就会发现不能再暂停了——没有更多的 yield 了。它最后再返回一次值:包含 valuedone 的对象。这时,value 的值为 undefined,而 donetrue。因为执行过程已经到达函数尾部,而没有发现其他的 yield。Node 就是通过这种方式知道整个 generator 函数已运行完成的。如果继续调用 next,就会抛出异常了:Error: Generator has already finished。

 

如何使用 generator

不要把 generator 函数和 generator 对象弄混了,下面讲讲区分它们的方法,因为如果不弄清楚了,它们确实容易混淆。

区分方法就是: next 方法调用并不作用在 generator 函数上,而是在 generator 对象上。Generator 函数的作用是,当被调用时,返回这个 generator 对象。下面的代码可以很好地解释:

function *generatorFunction() { … };

1
2
3
4
5
6
<span class="rem">// 错误写法</span>
generatorFunction.next();

<span class="rem">// 正确的写法</span>
<span class="kwrd">var</span> generatorObject = generatorFunction();
generatorObject.next();

 

return 语句终止 generator 函数的运行

如果需要立即停止 generator 函数,只需要在函数里使用 return 语句。如果有调用方调用了 next,函数在内部到达了 return,那么将会向调用方返回return 后面表达式的值,而 done 则为 true。请看这个例子:

function *generatorFunction() { yield “hello”; return “goodbye”; yield “you’ll never see me”; };

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<span class="kwrd">var</span> generatorObject = generatorFunction();

generatorObject.next();
<span class="rem">// => { value: 'hello', done: false }</span>

generatorObject.next();
<span class="rem">// => { value: 'goodbye', done: true }</span>

generatorObject.next();
<span class="rem">// => 异常: Generator has already finished.</span>

 

向 generator 函数传递参数

如果向 generator 函数传递参数,那在整个 generator 函数的生命周期内都可见。

function *generatorFunction(arg) { yield arg; yield arg; };

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<span class="kwrd">var</span> generatorObject = generatorFunction(<span class="str">"Hello, World!"</span>);

generatorObject.next();
<span class="rem">// => { value: 'Hello, World!', done: false }</span>

generatorObject.next();
<span class="rem">// => { value: 'Hello, World!', done: false }</span>

generatorObject.next();
<span class="rem">// => { value: undefined, done: true }</span>

 

generator 的高级话题

好吧,其实接下来的内容也并不是真的多么“高级”。我只是想要将它们与上面的基础区分开来,因为下面一些东西有点绕。它们不太直观,我学习它们的时候也是靠实验。所以,它们很有用,但有的概念可能需要多读几遍。

 

next 方法传递参数

如果向 next 方法传递参数,其值将作为函数中 yield 表达式的值。那么,通过 generator 对象,你可以传入一个新的参数值,并将其赋值回 generator 函数里(一会儿有例子)。

关于 generator 的第一个困惑就来了:暂停的说法好像并不很直观。每当被调用 next,会发生:

  1. 查找第一个 yield
  2. yield 后的表达式的值作为本次 next 调用的结果的值(value
  3. 恰好暂停在 yield

注意,是恰好暂停在 yield 处。即便 yield 这一行有其他代码,也不会执行。 如果不用示例,确实有点儿难解释清楚,那就来一发吧:

function *generatorFunction() { var arg1 = yield “First Pause!”;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  console.log(<span class="str">"Argument from Second Next: "</span> + arg1);

  <span class="kwrd">var</span> arg2 = yield <span class="str">"Second Pause"</span>;

  console.log(<span class="str">"Argument from Third Next: "</span> + arg2);
};

<span class="kwrd">var</span> generatorObject = generatorFunction();

generatorObject.next(<span class="str">"Argument A"</span>);
<span class="rem">// => { value: 'First Pause!', done: false }</span>

generatorObject.next(<span class="str">"Argument B"</span>);
<span class="rem">// => Argument from Second Next: Argument B</span>
<span class="rem">// => { value: 'Second Pause', done: false }</span>

generatorObject.next(<span class="str">"Argument C"</span>);
<span class="rem">// => Argument from Third Next: Argument C</span>
<span class="rem">// => { value: undefined, done: true }</span>

嗯?“*Argument A*” 传入进去之后去哪儿了? 显然,它被忽略并丢弃了。因为首次调用 generator 对象的 next 方法时,会运行函数并在第一个 yield 处停往。上面说到,当到达 yield 时,它不会执行任何其他东西——就算将 yield 表达式的值(d)赋值给变量的操作,也会被暂停住。在本例中,也就是连var arg1= 这个赋值操作也被停住了。这就是为什么令人困惑:它确实就是恰好在那个位置暂停了!

所以,当我们第二次调用 next,它就会使用传入的参数(这时传入的是 “*Argument B*”),作为 yield 表达式的值,赋值给了 arg1。接下来 console.log 运行,找到接下来的 yield 并再次停住(e)。我们第二次调用 next 时的返回值就是第二个 yield 后面的 “*Second Pause*”。

最后,再调用 next 就会完成 generator 函数的执行。

 

generator 链

有时候,你可能要在 generator 函数中调用另一个 generator 函数。这是一种元操作。要实现它,你要有一个主 generator 函数,来组合有链式调用的多个 generator 函数。

基于对上文的理解,你可能感觉这很简单:“只需要将从 next 方法返回的 generator 对象里提取嵌套的 generator 函数,并继续调用它的 next 方法,重复这个过程”,就像这样:

function *firstGeneratorFunction() { yield secondGeneratorFunction(); };

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<span class="kwrd">function</span> *secondGeneratorFunction() {
  yield thirdGeneratorFunction();
};

<span class="kwrd">function</span> *thirdGeneratorFunction() {
  console.log(<span class="str">"Inside the third generator!"</span>);
};

<span class="kwrd">var</span> generatorObject = firstGeneratorFunction();
<span class="kwrd">var</span> objectFromNext = generatorObject.next();
<span class="rem">// (此处的 value 是调用 secondGeneratorFunction() 所得的 generator 对象);</span>
<span class="rem">// => { value: {}, done: false }</span>

<span class="kwrd">var</span> objectFromNextAgain = objectFromNext.value.next();
<span class="rem">// (此处的 value 是调用 thirdGeneratorFunction() 所得的 generator 对象);</span>
<span class="rem">// => { value: {}, done: false }</span>

objectFromNextAgain.value.next();
<span class="rem">// => Inside the third generator!</span>

好吧,重要的是,这很晕,令人迷惑。所以,不应该像这样来构建 generator 函数链。Node 提供了一种方式:只需要一个 generator 函数,并且只需要在这个 generator 上调用 next。做法就是,在 yield 之后写一个星号(*)。

function *firstGeneratorFunction() { yield *secondGeneratorFunction(); };

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<span class="kwrd">function</span> *secondGeneratorFunction() {
  yield *thirdGeneratorFunction();
};

<span class="kwrd">function</span> *thirdGeneratorFunction() {
  yield *fourthGeneratorFunction();
};

<span class="kwrd">function</span> *fourthGeneratorFunction() {
  console.log(<span class="str">"Inside the fourth generator!"</span>);
};

<span class="kwrd">var</span> generatorObject = firstGeneratorFunction();
generatorObject.next()

<span class="rem">// => Inside the fourth generator!</span>
<span class="rem">// => { value: undefined, done: true }</span>

那么,你可能感觉奇怪,“为什么 done 的值立刻就变成了 true?其他那些 yield 起什么作用?” 这就是星号的作用。它就像一个 “解包” 操作,将 next调用操作传入嵌套的 generator 上。 由于我们一直解包再解包,调用被很快递到了 fourthGeneratorFunction 那里。

 

总结

以上。

这就是我从 generators 的体验中学到的。它们有点绕,不过大有裨益。如果你想找一个完整成型的例子,去看看 Koa 吧。Koa 将 generator 作为解决回调大吭的方法。在 generator 的处理上,它有点像 Connect 在 Web 应用项目中角色。(我计划写一篇关于理解 Koa 的文章)

 

我的处女作

这是我第一篇技术博客,所以,我十分期待大家的反馈。如果喜欢我解释问题的方式,请你们告诉我;如果感觉我并没有将问题解释明白,也请让我知道(f)

 


陈计节翻译注解

(a) generator 是 EcmaScript 6 中定义的语法,并非 node.js 特性。
(b) 也就是 不像正常函数那样运行函数并根据函数逻辑得出值)
© 表示函数执行尚未完成
(d) 即下一次 next 的传入值)
(e) 原文错误,原文意为“找到接下来的 next 并暂停”。
(f) 读者如果要讨论,可以去原作与作者交流,亦可在本文下方置评。)