Generator

所有的函数都会运行至完成,对吧?换句话说,一旦一个函数开始运行,在它完成之前没有任何东西能够打断它。

至少对于到目前为止的JavaScript的整个历史来说是这样的。在ES6中,引入了一个有些异乎寻常的新形式的函数,称为generator。一个generator可以在运行期间暂停它自己,还可以立即或者稍后继续运行。所以显然它没有普通函数那样的运行至完成的保证。

另外,在运行期间的每次暂停/继续轮回都是一个双向消息传递的好机会,generator可以在这里返回一个值,而使它继续的控制端代码可以发回一个值。

就像前一节中的迭代器一样,有种方式可以考虑generator是什么,或者说它对什么最有用。对此没有一个正确的答案,但我们将试着从几个角度考虑。

注意: 关于generator的更多信息参见本系列的 异步与性能,还可以参见本书的第四章。

语法

generator函数使用这种新语法声明:

  1. function *foo() {
  2. // ..
  3. }

*的位置在功能上无关紧要。同样的声明还可以写做以下的任意一种:

  1. function *foo() { .. }
  2. function* foo() { .. }
  3. function * foo() { .. }
  4. function*foo() { .. }
  5. ..

这里 唯一 的区别就是风格的偏好。大多数其他的文献似乎喜欢function* foo(..) { .. }。我喜欢function *foo(..) { .. },所以这就是我将在本书剩余部分中表示它们的方法。

我这样做的理由实质上纯粹是为了教学。在这本书中,当我引用一个generator函数时,我将使用*foo(..),与普通函数的foo(..)相对。我发现*foo(..)function *foo(..) { .. }*的位置更加吻合。

另外,就像我们在第二章的简约方法中看到的,在对象字面量中有一种简约generator形式:

  1. var a = {
  2. *foo() { .. }
  3. };

我要说在简约generator中,*foo() { .. }要比* foo() { .. }更自然。这进一步表明了为何使用*foo()匹配一致性。

一致性使理解与学习更轻松。

执行一个Generator

虽然一个generator使用*进行声明,但是你依然可以像一个普通函数那样执行它:

  1. foo();

你依然可以传给它参数值,就像:

  1. function *foo(x,y) {
  2. // ..
  3. }
  4. foo( 5, 10 );

主要区别在于,执行一个generator,比如foo(5,10),并不实际运行generator中的代码。取而代之的是,它生成一个迭代器来控制generator执行它的代码。

我们将在稍后的“迭代器控制”中回到这个话题,但是简要地说:

  1. function *foo() {
  2. // ..
  3. }
  4. var it = foo();
  5. // 要开始/推进`*foo()`,调用
  6. // `it.next(..)`

yield

Generator还有一个你可以在它们内部使用的新关键字,用来表示暂停点:yield。考虑如下代码:

  1. function *foo() {
  2. var x = 10;
  3. var y = 20;
  4. yield;
  5. var z = x + y;
  6. }

在这个*foo()generator中,前两行的操作将会在开始时运行,然后yield将会暂停这个generator。如果这个generator被继续,*foo()的最后一行将运行。在一个generator中yield可以出现任意多次(或者,在技术上讲,根本不出现!)。

你甚至可以在一个循环内部放置yield,它可以表示一个重复的暂停点。事实上,一个永不完成的循环就意味着一个永不完成的generator,这是完全合法的,而且有时候完全是你需要的。

yield不只是一个暂停点。它是在暂停generator时发送出一个值的表达式。这里是一个位于generator中的while..true循环,它每次迭代时yield出一个新的随机数:

  1. function *foo() {
  2. while (true) {
  3. yield Math.random();
  4. }
  5. }

yield ..表达式不仅发送一个值 —— 不带值的yieldyield undefined相同 —— 它还接收(也就是,被替换为)最终的继续值。考虑如下代码:

  1. function *foo() {
  2. var x = yield 10;
  3. console.log( x );
  4. }

这个generator在暂停它自己时将首先yield出值10。当你继续这个generator时 —— 使用我们先前提到的it.next(..) —— 无论你使用什么值继续它,这个值都将替换/完成整个表达式yield 10,这意味着这个值将被赋值给变量x

一个yield..表达式可以出现在任意普通表达式可能出现的地方。例如:

  1. function *foo() {
  2. var arr = [ yield 1, yield 2, yield 3 ];
  3. console.log( arr, yield 4 );
  4. }

这里的*foo()有四个yield ..表达式。其中每个yield都会导致generator暂停以等待一个继续值,这个继续值稍后被用于各个表达式环境中。

yield在技术上讲不是一个操作符,虽然像yield 1这样使用时看起来确实很像。因为yield可以像var x = yield这样完全通过自己被使用,所以将它认为是一个操作符有时令人困惑。

从技术上讲,yield ..a = 3这样的赋值表达式拥有相同的“表达式优先级” —— 概念上和操作符优先级很相似。这意味着yield ..基本上可以出现在任何a = 3可以合法出现的地方。

让我们展示一下这种对称性:

  1. var a, b;
  2. a = 3; // 合法
  3. b = 2 + a = 3; // 不合法
  4. b = 2 + (a = 3); // 合法
  5. yield 3; // 合法
  6. a = 2 + yield 3; // 不合法
  7. a = 2 + (yield 3); // 合法

注意: 如果你好好考虑一下,认为一个yield ..表达式与一个赋值表达式的行为相似在概念上有些道理。当一个被暂停的generator被继续时,它就以一种与被这个继续值“赋值”区别不大的方式,被这个值完成/替换。

要点:如果你需要yield ..出现在a = 3这样的赋值本不被允许出现的位置,那么它就需要被包在一个( )中。

因为yield关键字的优先级很低,几乎任何出现在yield ..之后的表达式都会在被yield发送之前首先被计算。只有扩散操作符...和逗号操作符,拥有更低的优先级,这意味着他们会在yield已经被求值之后才会被处理。

所以正如带有多个操作符的普通语句一样,存在另一个可能需要( )来覆盖(提升)yield的低优先级的情况,就像这些表达式之间的区别:

  1. yield 2 + 3; // 与`yield (2 + 3)`相同
  2. (yield 2) + 3; // 首先`yield 2`,然后`+ 3`

=赋值一样,yield也是“右结合性”的,这意味着多个接连出现的yield表达式被视为从右到左被( .. )分组。所以,yield yield yield 3将被视为yield (yield (yield 3))。像((yield) yield) yield 3这样的“左结合性”解释没有意义。

和其他操作符一样,yield与其他操作符或yield组合时为了使你的意图没有歧义,使用( .. )分组是一个好主意,即使这不是严格要求的。

注意: 更多关于操作符优先级和结合性的信息,参见本系列的 类型与文法

yield *

*使一个function声明成为一个function *generator声明的方式一样,一个*使yield成为一个机制非常不同的yield *,称为 yield委托。从文法上讲,yield *..的行为与yield ..相同,就像在前一节讨论过的那样。

yield * ..需要一个可迭代对象;然后它调用这个可迭代对象的迭代器,并将它自己的宿主generator的控制权委托给那个迭代器,直到它被耗尽。考虑如下代码:

  1. function *foo() {
  2. yield *[1,2,3];
  3. }

注意: 与generator声明中*的位置(早先讨论过)一样,在yield *表达式中的*的位置在风格上由你来决定。大多数其他文献偏好yield* ..,但是我喜欢yield *..,理由和我们已经讨论过的相同。

[1,2,3]产生一个将会步过它的值的迭代器,所以generator*foo()将会在被消费时产生这些值。另一种说明这种行为的方式是,yield委托到了另一个generator:

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. function *bar() {
  7. yield *foo();
  8. }

*bar()调用*foo()产生的迭代器通过yield *受到委托,意味着无论*foo()产生什么值都会被*bar()产生。

yield ..中表达式的完成值来自于使用it.next(..)继续generator,而yield *..表达式的完成值来自于受到委托的迭代器的返回值(如果有的话)。

内建的迭代器一般没有返回值,正如我们在本章早先的“迭代器循环”一节的末尾讲过的。但是如果你定义你自己的迭代器(或者generator),你就可以将它设计为return一个值,yield *..将会捕获它:

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. return 4;
  6. }
  7. function *bar() {
  8. var x = yield *foo();
  9. console.log( "x:", x );
  10. }
  11. for (var v of bar()) {
  12. console.log( v );
  13. }
  14. // 1 2 3
  15. // x: 4

虽然值12,和3*foo()中被yield出来,然后从*bar()中被yield出来,但是从*foo()中返回的值4是表达式yield *foo()的完成值,然后它被赋值给x

因为yield *可以调用另一个generator(通过委托到它的迭代器的方式),它还可以通过调用自己来实施某种generator递归:

  1. function *foo(x) {
  2. if (x < 3) {
  3. x = yield *foo( x + 1 );
  4. }
  5. return x * 2;
  6. }
  7. foo( 1 );

取得foo(1)的结果并调用迭代器的next()来使它运行它的递归步骤,结果将是24。第一次*foo()运行时x拥有值1,它是x < 3x + 1被递归地传递到*foo(..),所以之后的x2。再一次递归调用导致x3

现在,因为x < 3失败了,递归停止,而且return 3 * 26给回前一个调用的yeild *..表达式,它被赋值给x。另一个return 6 * 2返回12给前一个调用的x。最终12 * 2,即24,从generator*foo(..)运行的完成中被返回。

迭代器控制

早先,我们简要地介绍了generator是由迭代器控制的概念。现在让我们完整地深入这个话题。

回忆一下前一节的递归*for(..)。这是我们如何运行它:

  1. function *foo(x) {
  2. if (x < 3) {
  3. x = yield *foo( x + 1 );
  4. }
  5. return x * 2;
  6. }
  7. var it = foo( 1 );
  8. it.next(); // { value: 24, done: true }

在这种情况下,generator并没有真正暂停过,因为这里没有yield ..表达式。而yield *只是通过递归调用保持当前的迭代步骤继续运行下去。所以,仅仅对迭代器的next()函数进行一次调用就完全地运行了generator。

现在让我们考虑一个有多个步骤并且因此有多个产生值的generator:

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }

我们已经知道我们可以是使用一个for..of循环来消费一个迭代器,即便它是一个附着在*foo()这样的generator上:

  1. for (var v of foo()) {
  2. console.log( v );
  3. }
  4. // 1 2 3

注意: for..of循环需要一个可迭代对象。一个generator函数引用(比如foo)本身不是一个可迭代对象;你必须使用foo()来执行它以得到迭代器(它也是一个可迭代对象,正如我们在本章早先讲解过的)。理论上你可以使用一个实质上仅仅执行return this()Symbol.iterator函数来扩展GeneratorPrototype(所有generator函数的原型)。这将使foo引用本身成为一个可迭代对象,也就意味着for (var v of foo) { .. }(注意在foo上没有())将可以工作。

让我们手动迭代这个generator:

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. var it = foo();
  7. it.next(); // { value: 1, done: false }
  8. it.next(); // { value: 2, done: false }
  9. it.next(); // { value: 3, done: false }
  10. it.next(); // { value: undefined, done: true }

如果你仔细观察,这里有三个yield语句和四个next()调用。这可能看起来像是一个奇怪的不匹配。事实上,假定所有的东西都被求值并且generator完全运行至完成的话,next()调用将总是比yield表达式多一个。

但是如果你相反的角度观察(从里向外而不是从外向里),yieldnext()之间的匹配就显得更有道理。

回忆一下,yield ..表达式将被你用于继续generator的值完成。这意味着你传递给next(..)的参数值将完成任何当前暂停中等待完成的yield ..表达式。

让我们这样展示一下这种视角:

  1. function *foo() {
  2. var x = yield 1;
  3. var y = yield 2;
  4. var z = yield 3;
  5. console.log( x, y, z );
  6. }

在这个代码段中,每个yield ..都送出一个值(123),但更直接的是,它暂停了generator来等待一个值。换句话说,它就像在问这样一个问题,“我应当在这里用什么值?我会在这里等你告诉我。”

现在,这是我们如何控制*foo()来启动它:

  1. var it = foo();
  2. it.next(); // { value: 1, done: false }

这第一个next()调用从generator初始的暂停状态启动了它,并运行至第一个yield。在你调用第一个next()的那一刻,并没有yield ..表达式等待完成。如果你给第一个next()调用传递一个值,目前它会被扔掉,因为没有yield等着接受这样的一个值。

注意: 一个“ES6之后”时间表中的早期提案 允许你在generator内部通过一个分离的元属性(见第七章)来访问一个被传入初始next(..)调用的值。

现在,让我们回答那个未解的问题,“我应当给x赋什么值?” 我们将通过给 下一个 next(..)调用发送一个值来回答:

  1. it.next( "foo" ); // { value: 2, done: false }

现在,x将拥有值"foo",但我们也问了一个新的问题,“我应当给y赋什么值?”

  1. it.next( "bar" ); // { value: 3, done: false }

答案给出了,另一个问题被提出了。最终答案:

  1. it.next( "baz" ); // "foo" "bar" "baz"
  2. // { value: undefined, done: true }

现在,每一个yield ..的“问题”是如何被 下一个 next(..)调用回答的,所以我们观察到的那个“额外的”next()调用总是使一切开始的那一个。

让我们把这些步骤放在一起:

  1. var it = foo();
  2. // 启动generator
  3. it.next(); // { value: 1, done: false }
  4. // 回答第一个问题
  5. it.next( "foo" ); // { value: 2, done: false }
  6. // 回答第二个问题
  7. it.next( "bar" ); // { value: 3, done: false }
  8. // 回答第三个问题
  9. it.next( "baz" ); // "foo" "bar" "baz"
  10. // { value: undefined, done: true }

在生成器的每次迭代都简单地为消费者生成一个值的情况下,你可认为一个generator是一个值的生成器。

但是在更一般的意义上,也许将generator认为是一个受控制的,累进的代码执行过程更恰当,与早先“自定义迭代器”一节中的tasks队列的例子非常相像。

注意: 这种视角正是我们将如何在第四章中重温generator的动力。特别是,next(..)没有理由一定要在前一个next(..)完成之后立即被调用。虽然generator的内部执行环境被暂停了,程序的其他部分仍然没有被阻塞,这包括控制generator什么时候被继续的异步动作能力。

提前完成

正如我们在本章早先讲过的,连接到一个generator的迭代器支持可选的return(..)throw(..)方法。它们俩都有立即中止一个暂停的的generator的效果。

考虑如下代码:

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. var it = foo();
  7. it.next(); // { value: 1, done: false }
  8. it.return( 42 ); // { value: 42, done: true }
  9. it.next(); // { value: undefined, done: true }

return(x)有点像强制一个return x就在那个时刻被处理,这样你就立即得到这个指定的值。一旦一个generator完成,无论是正常地还是像展示的那样提前地,它就不再处理任何代码或返回任何值了。

return(..)除了可以手动调用,它还在迭代的最后被任何ES6中消费迭代器的结构自动调用,比如for..of循环和...扩散操作符。

这种能力的目的是,在控制端的代码不再继续迭代generator时它可以收到通知,这样它就可能做一些清理工作(释放资源,复位状态,等等)。与普通函数的清理模式完全相同,达成这个目的的主要方法是使用一个finally子句:

  1. function *foo() {
  2. try {
  3. yield 1;
  4. yield 2;
  5. yield 3;
  6. }
  7. finally {
  8. console.log( "cleanup!" );
  9. }
  10. }
  11. for (var v of foo()) {
  12. console.log( v );
  13. }
  14. // 1 2 3
  15. // cleanup!
  16. var it = foo();
  17. it.next(); // { value: 1, done: false }
  18. it.return( 42 ); // cleanup!
  19. // { value: 42, done: true }

警告: 不要把yield语句放在finally子句内部!它是有效和合法的,但这确实是一个可怕的主意。它在某种意义上推迟了return(..)调用的完成,因为在finally子句中的任何yield ..表达式都被遵循来暂停和发送消息;你不会像期望的那样立即得到一个完成的generator。基本上没有任何好的理由去选择这种疯狂的 坏的部分,所以避免这么做!

前一个代码段除了展示return(..)如何在中止generator的同时触发finally子句,它还展示了一个generator在每次被调用时都产生一个全新的迭代器。事实上,你可以并发地使用连接到相同generator的多个迭代器:

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. var it1 = foo();
  7. it1.next(); // { value: 1, done: false }
  8. it1.next(); // { value: 2, done: false }
  9. var it2 = foo();
  10. it2.next(); // { value: 1, done: false }
  11. it1.next(); // { value: 3, done: false }
  12. it2.next(); // { value: 2, done: false }
  13. it2.next(); // { value: 3, done: false }
  14. it2.next(); // { value: undefined, done: true }
  15. it1.next(); // { value: undefined, done: true }

提前中止

你可以调用throw(..)来代替return(..)调用。就像return(x)实质上在generator当前的暂停点上注入了一个return x一样,调用throw(x)实质上就像在暂停点上注入了一个throw x

除了处理异常的行为(我们在下一节讲解这对try子句意味着什么),throw(..)产生相同的提前完成 —— 在generator当前的暂停点中止它的运行。例如:

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. var it = foo();
  7. it.next(); // { value: 1, done: false }
  8. try {
  9. it.throw( "Oops!" );
  10. }
  11. catch (err) {
  12. console.log( err ); // Exception: Oops!
  13. }
  14. it.next(); // { value: undefined, done: true }

因为throw(..)基本上注入了一个throw ..来替换generator的yield 1这一行,而且没有东西处理这个异常,它立即传播回外面的调用端代码,调用端代码使用了一个try..catch来处理了它。

return(..)不同的是,迭代器的throw(..)方法绝不会被自动调用。

当然,虽然没有在前面的代码段中展示,但如果当你调用throw(..)时有一个try..finally子句等在generator内部的话,这个finally子句将会在异常被传播回调用端代码之前有机会运行。

错误处理

正如我们已经得到的提示,generator中的错误处理可以使用try..catch表达,它在上行和下行两个方向都可以工作。

  1. function *foo() {
  2. try {
  3. yield 1;
  4. }
  5. catch (err) {
  6. console.log( err );
  7. }
  8. yield 2;
  9. throw "Hello!";
  10. }
  11. var it = foo();
  12. it.next(); // { value: 1, done: false }
  13. try {
  14. it.throw( "Hi!" ); // Hi!
  15. // { value: 2, done: false }
  16. it.next();
  17. console.log( "never gets here" );
  18. }
  19. catch (err) {
  20. console.log( err ); // Hello!
  21. }

错误也可以通过yield *委托在两个方向上传播:

  1. function *foo() {
  2. try {
  3. yield 1;
  4. }
  5. catch (err) {
  6. console.log( err );
  7. }
  8. yield 2;
  9. throw "foo: e2";
  10. }
  11. function *bar() {
  12. try {
  13. yield *foo();
  14. console.log( "never gets here" );
  15. }
  16. catch (err) {
  17. console.log( err );
  18. }
  19. }
  20. var it = bar();
  21. try {
  22. it.next(); // { value: 1, done: false }
  23. it.throw( "e1" ); // e1
  24. // { value: 2, done: false }
  25. it.next(); // foo: e2
  26. // { value: undefined, done: true }
  27. }
  28. catch (err) {
  29. console.log( "never gets here" );
  30. }
  31. it.next(); // { value: undefined, done: true }

*foo()调用yield 1时,值1原封不动地穿过了*bar(),就像我们已经看到过的那样。

但这个代码段最有趣的部分是,当*foo()调用throw "foo: e2"时,这个错误传播到了*bar()并立即被*bar()try..catch块儿捕获。错误没有像值1那样穿过*bar()

然后*bar()catcherr普通地输出("foo: e2")之后*bar()就正常结束了,这就是为什么迭代器结果{ value: undefined, done: true }it.next()中返回。

如果*bar()没有用try..catch环绕着yield *..表达式,那么错误将理所当然地一直传播出来,而且在它传播的路径上依然会完成(中止)*bar()

转译一个Generator

有可能在ES6之前的环境中表达generator的能力吗?事实上是可以的,而且有好几种了不起的工具在这么做,包括最著名的Facebook的Regenerator工具 (https://facebook.github.io/regenerator/)。

但为了更好地理解generator,让我们试着手动转换一下。基本上讲,我们将制造一个简单的基于闭包的状态机。

我们将使原本的generator非常简单:

  1. function *foo() {
  2. var x = yield 42;
  3. console.log( x );
  4. }

开始之前,我们将需要一个我们能够执行的称为foo()的函数,它需要返回一个迭代器:

  1. function foo() {
  2. // ..
  3. return {
  4. next: function(v) {
  5. // ..
  6. }
  7. // 我们将省略`return(..)`和`throw(..)`
  8. };
  9. }

现在,我们需要一些内部变量来持续跟踪我们的“generator”的逻辑走到了哪一个步骤。我们称它为state。我们将有三种状态:起始状态的0,等待完成yield表达式的1,和generator完成的2

每次next(..)被调用时,我们需要处理下一个步骤,然后递增state。为了方便,我们将每个步骤放在一个switch语句的case子句中,并且我们将它放在一个next(..)可以调用的称为nextState(..)的内部函数中。另外,因为x是一个横跨整个“generator”作用域的变量,所以它需要存活在nextState(..)函数的外部。

这是将它们放在一起(很明显,为了使概念的展示更清晰,它经过了某些简化):

  1. function foo() {
  2. function nextState(v) {
  3. switch (state) {
  4. case 0:
  5. state++;
  6. // `yield`表达式
  7. return 42;
  8. case 1:
  9. state++;
  10. // `yield`表达式完成了
  11. x = v;
  12. console.log( x );
  13. // 隐含的`return`
  14. return undefined;
  15. // 无需处理状态`2`
  16. }
  17. }
  18. var state = 0, x;
  19. return {
  20. next: function(v) {
  21. var ret = nextState( v );
  22. return { value: ret, done: (state == 2) };
  23. }
  24. // 我们将省略`return(..)`和`throw(..)`
  25. };
  26. }

最后,让我们测试一下我们的前ES6“generator”:

  1. var it = foo();
  2. it.next(); // { value: 42, done: false }
  3. it.next( 10 ); // 10
  4. // { value: undefined, done: true }

不赖吧?希望这个练习能在你的脑中巩固这个概念:generator实际上只是状态机逻辑的简单语法。这使它们可以广泛地应用。

Generator的使用

我们现在非常深入地理解了generator如何工作,那么,它们在什么地方有用?

我们已经看过了两种主要模式:

  • 生产一系列值: 这种用法可以很简单(例如,随机字符串或者递增的数字),或者它也可以表达更加结构化的数据访问(例如,迭代一个数据库查询结果的所有行)。

    这两种方式中,我们使用迭代器来控制generator,这样就可以为每次next(..)调用执行一些逻辑。在数据解构上的普通迭代器只不过生成值而没有任何控制逻辑。

  • 串行执行的任务队列: 这种用法经常用来表达一个算法中步骤的流程控制,其中每一步都要求从某些外部数据源取得数据。对每块儿数据的请求可能会立即满足,或者可能会异步延迟地满足。

    从generator内部代码的角度来看,在yield的地方,同步或异步的细节是完全不透明的。另外,这些细节被有意地抽象出去,如此就不会让这样的实现细节把各个步骤间自然的,顺序的表达搞得模糊不清。抽象还意味着实现可以被替换/重构,而根本不用碰generator中的代码。

当根据这些用法观察generator时,它们的含义要比仅仅是手动状态机的一种不同或更好的语法多多了。它们是一种用于组织和控制有序地生产与消费数据的强大工具。