本文翻译自 @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 对象,它有两个属性:value
和 done
。value
包含你 yeild
后面表达的值;done
则包含一个 bool 值,如果还有更多的 yield
需要执行,则为 false
©。(我自己也经常把这些返回值弄混淆,所以为了便于记忆,请重复提醒自己 “只有用 yield 才产生值”)
考虑 yield
就是一种类似于 return
的语句。它之所以类似 return
,是因为在 yield
后面的表达式的值会在外部调用 next
时返回给调用方。 不过与 return
不同的是,函数的运行并没有终止,你可以继续执行函数。
如果再次调用 next
,generatorObject
就会执行到第二个(也是最后一个)yield
,然后重复第一次调用 next
的过程。
最后,如果第三次调用 next
,generatorObject
就会发现不能再暂停了——没有更多的 yield
了。它最后再返回一次值:包含 value
和 done
的对象。这时,value
的值为 undefined
,而 done
为 true
。因为执行过程已经到达函数尾部,而没有发现其他的 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
,会发生:
- 查找第一个
yield
- 把
yield
后的表达式的值作为本次next
调用的结果的值(value
) - 恰好暂停在
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) 读者如果要讨论,可以去原作与作者交流,亦可在本文下方置评。)