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

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

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

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

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

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

    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 *

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

    注意: 与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)的结果并调用迭代器的来使它运行它的递归步骤,结果将是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. }

    注意: 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 }

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

    现在,每一个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什么时候被继续的异步动作能力。

    考虑如下代码:

    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 }

    提前中止

    你可以调用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. }
    7. yield 2;
    8. throw "Hello!";
    9. }
    10. var it = foo();
    11. it.next(); // { value: 1, done: false }
    12. try {
    13. it.throw( "Hi!" ); // Hi!
    14. // { value: 2, done: false }
    15. it.next();
    16. console.log( "never gets here" );
    17. }
    18. catch (err) {
    19. console.log( err ); // Hello!
    20. }

    错误也可以通过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()

    有可能在ES6之前的环境中表达generator的能力吗?事实上是可以的,而且有好几种了不起的工具在这么做,包括最著名的Facebook的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内部代码的角度来看,在yield的地方,同步或异步的细节是完全不透明的。另外,这些细节被有意地抽象出去,如此就不会让这样的实现细节把各个步骤间自然的,顺序的表达搞得模糊不清。抽象还意味着实现可以被替换/重构,而根本不用碰generator中的代码。

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