04 Generator

回调表达异步控制流程的两个关键缺陷:

  • 基于回调的异步不符合大脑对任务步骤的规划方式
  • 由于控制反转,回调并不是可信任或可组合的

上章介绍了Promise如何把回调的控制反转反转回来的,恢复了可信任性/可组合性。
现在回到一种顺序、看似同步的异步流程控制表达风格。使这种风格成为可能的“魔法”就是ES6生成器(Generator)

4.1 打破完整运行

JS开发者在代码中几乎普遍依赖的一个假定:一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。

不过ES6引入了一个新的函数类型,它并不符合这种运行到结束的特性。这类新的函数被称为生成器

var x = 1;
function foo(){
    x++;
    bar();
    console.log("x:", x );
}
function bar() {
    x++;
}
foo();   // x: 3

这个例子中,我们确信bar()会在x++和console.log(x)之间运行。但是,如果bar()并不在那里会怎样呢? 显然结果就会是2,而不是3.

如果在抢占式多线程语言中,从本质上说,这是可能发生的,bar()可在两个语句之间打断并运行。但JS并不是抢占式的,也不是多线程的。然而,如果foo()自身可通过某种形式在代码的这个位置指示暂停的话,那就仍然可以一种合作的方式实现这样的中断(并发)。

ES6指示暂停点的语法是yield,这也礼貌地表达了一种合作式的控制放弃。

var x = 1;
function *foo(){
    x++;
    yield;
    console.log( 'x:', x );
}
function bar() {
    x++;
}
// 构造一个迭代器it来控制这个生成器
var it = foo();

// 这里启动foo();
it.next();
x;   //2
bar();  
x;   //3
it.next();   x: 3

在解释ES6生成器的不同机制和语法前,先来看看运行过程

  1. it = foo() 运算并没有执行生成器 *foo(),而只是构造了一个迭代器(iterator),这个迭代器会控制它的执行。
  2. 第一个 it.next() 启动了生成器 *foo(),并运行了 *foo() 第一行的 x++
  3. foo() 在yield语句处暂停,在这一点上第一个it.next()调用结束。此时foo() 仍在运行并且是活跃的,但处于暂停状态。
  4. 我们查看x的值,此时为2
  5. 我们调用bar(),它通过x++再次递增x
  6. 我们再次查看x的值,此时为3
  7. 最后的it.next()调用从暂停处恢复了生成器 *foo()的执行,并运行console.log()语句,这条语句使用当前x的值3
因此,生成器就是一类特殊的函数,可一次或多次启动和停止,并不一定非得要完成。

4.1.1 输入和输出

function *foo(x,y){
    return x * y;
}
var it = foo(6, 7)
var res = it.next();
res.value; // 42

可看到生成器和普通函数在调用上的一个区别。显然foo(6, 7)看起来很熟悉。但 *foo() 并没有像普通函数一样实际运行。

事实上,我们只是创建了一个迭代器对象,把它赋给了一个变量it,用于控制生成器 *foo() 。然后调用it.next(),指示生成器 *foo() 从当前位置开始继续运行,停在下一个yield处或直到生成器结束。

这个next() 调用的结果是一个对象,它有一个value属性,持有从 *foo() 返回的值。换句话说,yield会导致生成器在执行过程中发送出一个值,这有点类似于中间的return。

1. 迭代消息传递

Generator提供了内建消息输入输出能力,通过yield 和 next() 实现

function *foo(x){
    var y = x * (yield);
    return y;
}
var it = foo( 6 );

// 启动foo()
it.next();
var res = it.next( 7 );
res.value;  //42

首先,传入6作为参数x。然后调用it.next(), 这会启动 *foo()

在 *foo()内部,开始执行语句 var y = x..,但随后就遇到了一个yield表达式。头就会在这一点上暂停 *foo(),并在本质上要求调用代码为 yield表达式提供一个结果值。接下来,调用 it.next(7),这一句把值 7 传回作为被暂停的 yield表达式的结果。所以,这时赋值语句实际上就是 var y = 6 * 7;

上面代码有一个 yield 和两个 next() 调用。为什么会有这个不匹配?

因为第一个next()总是启动一个生成器,并运行到第一个yield处。不过,是第二个next()调用完厂第一个被暂停的yield表达式,第三个next()调用完成第二个yield依此类推。

2. 两个问题的故事

只考虑生成器代码

var y = x * (yield);
return y;

第遇 yield 基本是推出了一个问题: “这里我应该插入什么值?”
谁来回答这个问题呢?第一个next()已经运行,使得生成器启动并运行到此处,所以显然它无法回答这个问题。因此必须由第二个next()调用回答第一个yield提出的这个问题。

把视角转化一下:从迭代器的角度
消息是双向传递的 - yield.. 作为一个表达式可发出消息响应next()调用,next()也可向暂停的yield表达式发数值,考虑下面:

function *foo(x){
    var y = x * (yield "Hello");   // yield
    return y;
}
var it = foo( 6 );
var res = it.next();  // 第一个next() ,并不传入任何东西
res.value;                //  “Hello”

res = it.next( 7 );  // 向等待的yield传入 7
res.value;  // 42

yield 和 next() 这一对组件起来,在生成器的执行过程中构成了一个双向消息传递系统。

var res = it.next();
res.value;                      // Hello

res = it.next(7);  
res.value;                    // 42

第一个next()调用基本上就是在提出一个问题:*foo()要给我的下一个值是什么?说来回答这个问题呢?第一个yield “hello”表达式。

根据你认为提出问题的是谁,yield和next()调用之间要么有不匹配,要么没有。

但是,稍等!与yield语句的数量相比,还是多出了一个额外的next()。所以,最后一个it.next(7) 调用再次推出了这样的问题:生成器将要产生的下一个值是什么。但是,再没有yield语句来回答这个问题了,是不是?那么谁来回答呢?

return 语句回答这个问题

如果你的生成器中没有return 的话 - 在生成器中和在普通函数中一样,return 当然不是必需的 - 总有一个假定的/隐式的return(也就是 return undefined),它会在默认情况下回答最后的it.next(7) 调用提出的问题。

多个迭代器

从语法使用的方面来看,通过 一个迭代器控制生成器的时候,似乎是在控制声明的生成器函数本身。但有一个细微之处很容易忽略:每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器实例。

同一个生成器的多个实例可同时运行,它们甚至可彼此交互

function *foo(){
    var x = yield 2;
    z++;
    var y = yield (x * z);
    console.log(x, y, z);
}

var z = 1;

var it1 = foo();
var it2 = foo();

var val1 = it1.next().value;        // 2 <-- yield 2
var val2 = it2.next().value;      // 2 <-- yield 2

val1 = it1.next( val2 * 10 ).value;    // 40 <-- x:20,  z:2
val2 = it2.next( val1 * 5 ).value;     // 600 <-- x:200, z:3

it1.next( val2 / 2 );   // y:300
                                  // 20 300 3
it2.next( val1 / 4 );   // y: 10
                                 // 200 10 3
  1. *foo() 的两个实例同时启动,两个next()分别从 yield 2 语句中得到值 2
  2. val2 * 10 也紧 2 * 10,发送到第遇生成器实例it1,因此x得到值20。z从1增加到2,然后20*2通过yield发出,将val1设置为40。
  3. val1 * 5 也就是 40 * 5,发送到第二个生成器实例 it2,因此x得到值200。z再次从2 递增到3,然后200 * 3通过yield发出,将val2设置为600。
  4. val2 / 2 也就是 600/2,发送到第一个生成器实例it1,因此y得到值300,然后打印出 x y z 的值分别是 20 300 3。
  5. val1 / 4 也就是 40/4,发送到第二个生成器实例it2,因此y得到值10,然后打印出x y z 的值分别为200 10 3
交替执行
var a = 1;
var b = 2;
function foo(){
    a++;
    b = b * a;
    a = b + 3;
}
function bar(){
    b--;
    a = 8 + b;
    b = a * 2;
}

如果是普通的JS函数的话,显然,要么是foo()首先运行完毕,要么是bar()首先运行完毕,但foo()的语句不能交替执行。所以,前面的程序只有两种可能的输出。
但是,使用生成器的话,交替执行显然是可能的

var a = 1;
var b = 2;
function *foo(){
    a++;
    yield;
    b = b * a;
    a = (yield b) + 3;
}

function *bar(){
    b--;
    yield;
    a = (yield 8) + b;
    b = a * (yield 2);
}

根据迭代器控制的*foo()和 *bar() 调用的相对顺序不同,前面的程序可能会产生多种不同的结果。换句话说,通过两个生成器在共享的相同变量上的迭代交替执行,我们实际上可印证第1章讨论的理论上的多线程竟态条件环境。

function step(gen){
    var it = gen();
    var last;
    return function {
        // 不管yield 出来的是什么,下一次都把它原样传回去
        last = it.next( last ).value; 
    }    
}

step()初始化了一个生成器来创建迭代器it,然后返回一个函数,这个函数被调用的时候会将迭代器向前迭代一步。另外,前面的yield发出的值会在下一步发送回去。于是,yield 8 就是 8,而yield b 就是 (yield 发出的值).

现在,来试验一下交替运行 *foo() 和 *bar()代码块的效果。我们从乏味的基本情况开始,确保 *foo() 在 *bar() 之前完全结束。

// 确保重新设置a和b
a = 1;
b = 2;

var s1 = step( foo );
var s2 = step( bar );

// 首次运行 *foo()
s1();   // a++
s1();   // b = 4
s1();   // a = 7

// 现在运行*bar()
s2();  // b--   -->3
s2();  // yield 8
s2();  // a = 8 + 3   ==>11
s2();  // b = 11 * 2   ==> 22

console.log(a, b);  // 11 22

现在交替执行顺序

a = 1;
b = 2;

var s1 = step( foo );
var s2 = step( bar );

s2();    // b--
s2();    // yield 8
s1();    // a++;
s2();    // a = 8 + b
         // yield 2
s1();    // b = b * a;
         // yield b
s1();   // a = b + 3;
s2();   // b = a * 2;

你可能感兴趣的:(04 Generator)