第九章 生成器
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()不在那但仍可以在x++和console.log(x)之间运行,要怎么实现?
如果在抢占式多线程语言,这是可能发生的,bar()可以在两个语句之间打断并运行,但js不是抢占式也非多进程,如果foo()自身可以通过某种形式在代码的该位置指示暂停,就可以以一种合作式的方式实现中断。
var x = 1;
function *foo(){
x++;
yield;
console.log("x:",x);
}
function bar(){
x++;
}
//要如何运行前面的代码片段,使得bar在*foo内部的yield处运行呢?
var it = foo();
it.next();
x;//2
bar();
x;//3
it.next();//x:3
运行过程如下:
- it=foo()并没有执行生成器foo(),而是构造了一个迭代器,这个迭代器会控制它的执行。
- 第一个it.next()启动了生成器,并运行了x++;
- foo()在yield处暂停,在这一点上第一个it.next()调用结束。此时foo()仍是运行并活跃的,但处于暂停状态。
- 查看x的值,为2
- 调用bar(),它通过x++再次递增x
- 查看x,为3
- 最后的it.next()调用从暂停处恢复了生成器*foo()的执行,并运行console.log
由此可见,生成器就是一类特殊函数,可以一次或多次启动和暂停,并不一定要完成。
(1)输入和输出
生成器作为一个函数有一些基本特性,比如可以接受参数(输入)也能够返回值(输出)。
function *foo(){
return x*y;
}
var it = foo(6,7);
var res = it.next();
res.value;
向foo()分别传入6,7,作为参数x,y。foo()向调用代码返回42。
此处可以看到生成器和普通函数的区别,它并没有像普通函数一样实际运行。然后调用it.next,指示生成器从当前位置开始继续运行,停在下一个yield处或者直到生成器结束。
这个next(..)调用的结果是一个对象,它有一个value属性,持有从*foo()返回的值。也就是说,yield会导致生成器在执行过程中发送出一个值,有点类似于return。
(1.1)迭代信息传递
除了能接受参数并提供返回值,生成器还能通过yield和next内建信息输入输出。
function *foo(){
var y = x * (yield);
return y;
}
var it = foo(6);
it.next();
var res = it.next(7);
res.value;//42
首先,传入6作为参数x,然后调用it.next(),启动生成器。
在生成器内部,开始执行 var y = x...但遇到了yield表达式,会在这一点上暂停foo(在复赋值句中间),并在本质上于要求调用代码为yield表达式提供一个结果值。
接下来,调用 it.next(7),把7传回作为被暂停的yield表达式的结果。
此时,赋值语句实际上是var y = 67现在return y返回值42作为调用it.next(7)的结果。
注意:根据视角不同,yield和next有一个不匹配,一般来说next语句比yield多一个,因为第一个next的作用是启动生成器并运行到第一个yield处,第二个next调用完成第一个被暂停的yield表达式,第三个next调用完成第二个yield,以此类型。
(1.2)两个问题的故事
只考虑生成器代码:
var y = x * (yield);
return y;
第一个yield基本上提出了一个问题:这里应该插入什么?
第一个next只负责启动生成器,这个问题只能交给第二个next调用来回答。(这里也体现了不匹配)
再从迭代器的角度看(生成器是构建迭代器的方式),消息传递是双向的,yield作为一个表达式可以发出消息响应next调用,next也可以向暂停的yield表达式发送值。
function *foo(x){
var y = x * (yield "hello");
return y;
}
var it = foo(6);
var res = it.next();//启动
res.value ;//hello
res = it.next(7);//向等待的yield传入7
res.value;//42
yield和next组合起来构成了一个双向消息传递系统。
看后四行迭代器代码,只有暂停的yield才能接受这样一个通过next传递的值,而在生成器的起始处我们调用第一个next时,还没有暂停的yield来接收值。规范和所有兼容浏览器都会默默丢掉传递给第一个next的任何东西,因此
第一个next调用基本上在提出一个问题:生成器*foo要给我的下一个值是什么?谁来回答,第一个yield表达式。这里没有不匹配,所以根据问题的提出者是谁,yield和next要么不匹配,要么没有。与yield相比,next还是多出一个,所以最后的it.next(7)提出了这样的问题:生成器要产生的下一个值是什么?但是再没有yield语句回答,此时,return语句会回答!如果没有return会在默认情况下回答最后的it.next(7)提出的问题。
(2)多个迭代器
每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的不是生成器函数本身,而是这个生成器的实例。
同一个生成器的多个实例可以同时运行,甚至可以交互
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
var val2 = it2.next().value;//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
执行流程:
- *foo()的两个实例同时启动,两个next分别从yield 2得到2
- val210得到20,发送到it1,因此x得到20(next向暂停的yield发送值,把x变为20),z变为2,然后202通过yield发出,将val1设置为40
- val15也就是405,发送到it2,因此x得到200,z从2变为3,然后200*3通过yield发出,将val2设置为600
- val2/2即600/2,发送到it1,因此y得到300,然后打印xyz分别20 300 3
- val1/4即40/4,发送给it2,因此y得到10,xyz分别是200 10 3
书中还举了两个例子非常利于理解yield和next。
心得:每两个next消耗一个yield;注意yield在a++之后,a还是原来的值,下一个next之后的语句使用的是新值。
网友评论