Generator
所有的函数都会运行至完成,对吧?换句话说,一旦一个函数开始运行,在它完成之前没有任何东西能够打断它。
至少对于到目前为止的JavaScript的整个历史来说是这样的。在ES6中,引入了一个有些异乎寻常的新形式的函数,称为generator。一个generator可以在运行期间暂停它自己,还可以立即或者稍后继续运行。所以显然它没有普通函数那样的运行至完成的保证。
另外,在运行期间的每次暂停/继续轮回都是一个双向消息传递的好机会,generator可以在这里返回一个值,而使它继续的控制端代码可以发回一个值。
就像前一节中的迭代器一样,有种方式可以考虑generator是什么,或者说它对什么最有用。对此没有一个正确的答案,但我们将试着从几个角度考虑。
注意: 关于generator的更多信息参见本系列的 异步与性能,还可以参见本书的第四章。
语法
generator函数使用这种新语法声明:
function *foo() {
// ..
}
*
的位置在功能上无关紧要。同样的声明还可以写做以下的任意一种:
function *foo() { .. }
function* foo() { .. }
function * foo() { .. }
function*foo() { .. }
..
这里 唯一 的区别就是风格的偏好。大多数其他的文献似乎喜欢function* foo(..) { .. }
。我喜欢function *foo(..) { .. }
,所以这就是我将在本书剩余部分中表示它们的方法。
我这样做的理由实质上纯粹是为了教学。在这本书中,当我引用一个generator函数时,我将使用*foo(..)
,与普通函数的foo(..)
相对。我发现*foo(..)
与function *foo(..) { .. }
中*
的位置更加吻合。
另外,就像我们在第二章的简约方法中看到的,在对象字面量中有一种简约generator形式:
var a = {
*foo() { .. }
};
我要说在简约generator中,*foo() { .. }
要比* foo() { .. }
更自然。这进一步表明了为何使用*foo()
匹配一致性。
一致性使理解与学习更轻松。
执行一个Generator
虽然一个generator使用*
进行声明,但是你依然可以像一个普通函数那样执行它:
foo();
你依然可以传给它参数值,就像:
function *foo(x,y) {
// ..
}
foo( 5, 10 );
主要区别在于,执行一个generator,比如foo(5,10)
,并不实际运行generator中的代码。取而代之的是,它生成一个迭代器来控制generator执行它的代码。
我们将在稍后的“迭代器控制”中回到这个话题,但是简要地说:
function *foo() {
// ..
}
var it = foo();
// 要开始/推进`*foo()`,调用
// `it.next(..)`
yield
Generator还有一个你可以在它们内部使用的新关键字,用来表示暂停点:yield
。考虑如下代码:
function *foo() {
var x = 10;
var y = 20;
yield;
var z = x + y;
}
在这个*foo()
generator中,前两行的操作将会在开始时运行,然后yield
将会暂停这个generator。如果这个generator被继续,*foo()
的最后一行将运行。在一个generator中yield
可以出现任意多次(或者,在技术上讲,根本不出现!)。
你甚至可以在一个循环内部放置yield
,它可以表示一个重复的暂停点。事实上,一个永不完成的循环就意味着一个永不完成的generator,这是完全合法的,而且有时候完全是你需要的。
yield
不只是一个暂停点。它是在暂停generator时发送出一个值的表达式。这里是一个位于generator中的while..true
循环,它每次迭代时yield
出一个新的随机数:
function *foo() {
while (true) {
yield Math.random();
}
}
yield ..
表达式不仅发送一个值 —— 不带值的yield
与yield undefined
相同 —— 它还接收(也就是,被替换为)最终的继续值。考虑如下代码:
function *foo() {
var x = yield 10;
console.log( x );
}
这个generator在暂停它自己时将首先yield
出值10
。当你继续这个generator时 —— 使用我们先前提到的it.next(..)
—— 无论你使用什么值继续它,这个值都将替换/完成整个表达式yield 10
,这意味着这个值将被赋值给变量x
一个yield..
表达式可以出现在任意普通表达式可能出现的地方。例如:
function *foo() {
var arr = [ yield 1, yield 2, yield 3 ];
console.log( arr, yield 4 );
}
这里的*foo()
有四个yield ..
表达式。其中每个yield
都会导致generator暂停以等待一个继续值,这个继续值稍后被用于各个表达式环境中。
yield
在技术上讲不是一个操作符,虽然像yield 1
这样使用时看起来确实很像。因为yield
可以像var x = yield
这样完全通过自己被使用,所以将它认为是一个操作符有时令人困惑。
从技术上讲,yield ..
与a = 3
这样的赋值表达式拥有相同的“表达式优先级” —— 概念上和操作符优先级很相似。这意味着yield ..
基本上可以出现在任何a = 3
可以合法出现的地方。
让我们展示一下这种对称性:
var a, b;
a = 3; // 合法
b = 2 + a = 3; // 不合法
b = 2 + (a = 3); // 合法
yield 3; // 合法
a = 2 + yield 3; // 不合法
a = 2 + (yield 3); // 合法
注意: 如果你好好考虑一下,认为一个yield ..
表达式与一个赋值表达式的行为相似在概念上有些道理。当一个被暂停的generator被继续时,它就以一种与被这个继续值“赋值”区别不大的方式,被这个值完成/替换。
要点:如果你需要yield ..
出现在a = 3
这样的赋值本不被允许出现的位置,那么它就需要被包在一个( )
中。
因为yield
关键字的优先级很低,几乎任何出现在yield ..
之后的表达式都会在被yield
发送之前首先被计算。只有扩散操作符...
和逗号操作符,
拥有更低的优先级,这意味着他们会在yield
已经被求值之后才会被处理。
所以正如带有多个操作符的普通语句一样,存在另一个可能需要( )
来覆盖(提升)yield
的低优先级的情况,就像这些表达式之间的区别:
yield 2 + 3; // 与`yield (2 + 3)`相同
(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的控制权委托给那个迭代器,直到它被耗尽。考虑如下代码:
function *foo() {
yield *[1,2,3];
}
注意: 与generator声明中*
的位置(早先讨论过)一样,在yield *
表达式中的*
的位置在风格上由你来决定。大多数其他文献偏好yield* ..
,但是我喜欢yield *..
,理由和我们已经讨论过的相同。
值[1,2,3]
产生一个将会步过它的值的迭代器,所以generator*foo()
将会在被消费时产生这些值。另一种说明这种行为的方式是,yield委托到了另一个generator:
function *foo() {
yield 1;
yield 2;
yield 3;
}
function *bar() {
yield *foo();
}
当*bar()
调用*foo()
产生的迭代器通过yield *
受到委托,意味着无论*foo()
产生什么值都会被*bar()
产生。
在yield ..
中表达式的完成值来自于使用it.next(..)
继续generator,而yield *..
表达式的完成值来自于受到委托的迭代器的返回值(如果有的话)。
内建的迭代器一般没有返回值,正如我们在本章早先的“迭代器循环”一节的末尾讲过的。但是如果你定义你自己的迭代器(或者generator),你就可以将它设计为return
一个值,yield *..
将会捕获它:
function *foo() {
yield 1;
yield 2;
yield 3;
return 4;
}
function *bar() {
var x = yield *foo();
console.log( "x:", x );
}
for (var v of bar()) {
console.log( v );
}
// 1 2 3
// x: 4
虽然值1
,2
,和3
从*foo()
中被yield
出来,然后从*bar()
中被yield
出来,但是从*foo()
中返回的值4
是表达式yield *foo()
的完成值,然后它被赋值给x
。
因为yield *
可以调用另一个generator(通过委托到它的迭代器的方式),它还可以通过调用自己来实施某种generator递归:
function *foo(x) {
if (x < 3) {
x = yield *foo( x + 1 );
}
return x * 2;
}
foo( 1 );
取得foo(1)
的结果并调用迭代器的next()
来使它运行它的递归步骤,结果将是24
。第一次*foo()
运行时x
拥有值1
,它是x < 3
。x + 1
被递归地传递到*foo(..)
,所以之后的x
是2
。再一次递归调用导致x
为3
。
现在,因为x < 3
失败了,递归停止,而且return 3 * 2
将6
给回前一个调用的yeild *..
表达式,它被赋值给x
。另一个return 6 * 2
返回12
给前一个调用的x
。最终12 * 2
,即24
,从generator*foo(..)
运行的完成中被返回。
迭代器控制
早先,我们简要地介绍了generator是由迭代器控制的概念。现在让我们完整地深入这个话题。
回忆一下前一节的递归*for(..)
。这是我们如何运行它:
function *foo(x) {
if (x < 3) {
x = yield *foo( x + 1 );
}
return x * 2;
}
var it = foo( 1 );
it.next(); // { value: 24, done: true }
在这种情况下,generator并没有真正暂停过,因为这里没有yield ..
表达式。而yield *
只是通过递归调用保持当前的迭代步骤继续运行下去。所以,仅仅对迭代器的next()
函数进行一次调用就完全地运行了generator。
现在让我们考虑一个有多个步骤并且因此有多个产生值的generator:
function *foo() {
yield 1;
yield 2;
yield 3;
}
我们已经知道我们可以是使用一个for..of
循环来消费一个迭代器,即便它是一个附着在*foo()
这样的generator上:
for (var v of foo()) {
console.log( v );
}
// 1 2 3
注意: for..of
循环需要一个可迭代对象。一个generator函数引用(比如foo
)本身不是一个可迭代对象;你必须使用foo()
来执行它以得到迭代器(它也是一个可迭代对象,正如我们在本章早先讲解过的)。理论上你可以使用一个实质上仅仅执行return this()
的Symbol.iterator
函数来扩展GeneratorPrototype
(所有generator函数的原型)。这将使foo
引用本身成为一个可迭代对象,也就意味着for (var v of foo) { .. }
(注意在foo
上没有()
)将可以工作。
让我们手动迭代这个generator:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
如果你仔细观察,这里有三个yield
语句和四个next()
调用。这可能看起来像是一个奇怪的不匹配。事实上,假定所有的东西都被求值并且generator完全运行至完成的话,next()
调用将总是比yield
表达式多一个。
但是如果你相反的角度观察(从里向外而不是从外向里),yield
和next()
之间的匹配就显得更有道理。
回忆一下,yield ..
表达式将被你用于继续generator的值完成。这意味着你传递给next(..)
的参数值将完成任何当前暂停中等待完成的yield ..
表达式。
让我们这样展示一下这种视角:
function *foo() {
var x = yield 1;
var y = yield 2;
var z = yield 3;
console.log( x, y, z );
}
在这个代码段中,每个yield ..
都送出一个值(1
,2
,3
),但更直接的是,它暂停了generator来等待一个值。换句话说,它就像在问这样一个问题,“我应当在这里用什么值?我会在这里等你告诉我。”
现在,这是我们如何控制*foo()
来启动它:
var it = foo();
it.next(); // { value: 1, done: false }
这第一个next()
调用从generator初始的暂停状态启动了它,并运行至第一个yield
。在你调用第一个next()
的那一刻,并没有yield ..
表达式等待完成。如果你给第一个next()
调用传递一个值,目前它会被扔掉,因为没有yield
等着接受这样的一个值。
注意: 一个“ES6之后”时间表中的早期提案 将 允许你在generator内部通过一个分离的元属性(见第七章)来访问一个被传入初始next(..)
调用的值。
现在,让我们回答那个未解的问题,“我应当给x
赋什么值?” 我们将通过给 下一个 next(..)
调用发送一个值来回答:
it.next( "foo" ); // { value: 2, done: false }
现在,x
将拥有值"foo"
,但我们也问了一个新的问题,“我应当给y
赋什么值?”
it.next( "bar" ); // { value: 3, done: false }
答案给出了,另一个问题被提出了。最终答案:
it.next( "baz" ); // "foo" "bar" "baz"
// { value: undefined, done: true }
现在,每一个yield ..
的“问题”是如何被 下一个 next(..)
调用回答的,所以我们观察到的那个“额外的”next()
调用总是使一切开始的那一个。
让我们把这些步骤放在一起:
var it = foo();
// 启动generator
it.next(); // { value: 1, done: false }
// 回答第一个问题
it.next( "foo" ); // { value: 2, done: false }
// 回答第二个问题
it.next( "bar" ); // { value: 3, done: false }
// 回答第三个问题
it.next( "baz" ); // "foo" "bar" "baz"
// { value: undefined, done: true }
在生成器的每次迭代都简单地为消费者生成一个值的情况下,你可认为一个generator是一个值的生成器。
但是在更一般的意义上,也许将generator认为是一个受控制的,累进的代码执行过程更恰当,与早先“自定义迭代器”一节中的tasks
队列的例子非常相像。
注意: 这种视角正是我们将如何在第四章中重温generator的动力。特别是,next(..)
没有理由一定要在前一个next(..)
完成之后立即被调用。虽然generator的内部执行环境被暂停了,程序的其他部分仍然没有被阻塞,这包括控制generator什么时候被继续的异步动作能力。
提前完成
正如我们在本章早先讲过的,连接到一个generator的迭代器支持可选的return(..)
和throw(..)
方法。它们俩都有立即中止一个暂停的的generator的效果。
考虑如下代码:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
it.return( 42 ); // { value: 42, done: true }
it.next(); // { value: undefined, done: true }
return(x)
有点像强制一个return x
就在那个时刻被处理,这样你就立即得到这个指定的值。一旦一个generator完成,无论是正常地还是像展示的那样提前地,它就不再处理任何代码或返回任何值了。
return(..)
除了可以手动调用,它还在迭代的最后被任何ES6中消费迭代器的结构自动调用,比如for..of
循环和...
扩散操作符。
这种能力的目的是,在控制端的代码不再继续迭代generator时它可以收到通知,这样它就可能做一些清理工作(释放资源,复位状态,等等)。与普通函数的清理模式完全相同,达成这个目的的主要方法是使用一个finally
子句:
function *foo() {
try {
yield 1;
yield 2;
yield 3;
}
finally {
console.log( "cleanup!" );
}
}
for (var v of foo()) {
console.log( v );
}
// 1 2 3
// cleanup!
var it = foo();
it.next(); // { value: 1, done: false }
it.return( 42 ); // cleanup!
// { value: 42, done: true }
警告: 不要把yield
语句放在finally
子句内部!它是有效和合法的,但这确实是一个可怕的主意。它在某种意义上推迟了return(..)
调用的完成,因为在finally
子句中的任何yield ..
表达式都被遵循来暂停和发送消息;你不会像期望的那样立即得到一个完成的generator。基本上没有任何好的理由去选择这种疯狂的 坏的部分,所以避免这么做!
前一个代码段除了展示return(..)
如何在中止generator的同时触发finally
子句,它还展示了一个generator在每次被调用时都产生一个全新的迭代器。事实上,你可以并发地使用连接到相同generator的多个迭代器:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it1 = foo();
it1.next(); // { value: 1, done: false }
it1.next(); // { value: 2, done: false }
var it2 = foo();
it2.next(); // { value: 1, done: false }
it1.next(); // { value: 3, done: false }
it2.next(); // { value: 2, done: false }
it2.next(); // { value: 3, done: false }
it2.next(); // { value: undefined, done: true }
it1.next(); // { value: undefined, done: true }
提前中止
你可以调用throw(..)
来代替return(..)
调用。就像return(x)
实质上在generator当前的暂停点上注入了一个return x
一样,调用throw(x)
实质上就像在暂停点上注入了一个throw x
。
除了处理异常的行为(我们在下一节讲解这对try
子句意味着什么),throw(..)
产生相同的提前完成 —— 在generator当前的暂停点中止它的运行。例如:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
try {
it.throw( "Oops!" );
}
catch (err) {
console.log( err ); // Exception: Oops!
}
it.next(); // { value: undefined, done: true }
因为throw(..)
基本上注入了一个throw ..
来替换generator的yield 1
这一行,而且没有东西处理这个异常,它立即传播回外面的调用端代码,调用端代码使用了一个try..catch
来处理了它。
与return(..)
不同的是,迭代器的throw(..)
方法绝不会被自动调用。
当然,虽然没有在前面的代码段中展示,但如果当你调用throw(..)
时有一个try..finally
子句等在generator内部的话,这个finally
子句将会在异常被传播回调用端代码之前有机会运行。
错误处理
正如我们已经得到的提示,generator中的错误处理可以使用try..catch
表达,它在上行和下行两个方向都可以工作。
function *foo() {
try {
yield 1;
}
catch (err) {
console.log( err );
}
yield 2;
throw "Hello!";
}
var it = foo();
it.next(); // { value: 1, done: false }
try {
it.throw( "Hi!" ); // Hi!
// { value: 2, done: false }
it.next();
console.log( "never gets here" );
}
catch (err) {
console.log( err ); // Hello!
}
错误也可以通过yield *
委托在两个方向上传播:
function *foo() {
try {
yield 1;
}
catch (err) {
console.log( err );
}
yield 2;
throw "foo: e2";
}
function *bar() {
try {
yield *foo();
console.log( "never gets here" );
}
catch (err) {
console.log( err );
}
}
var it = bar();
try {
it.next(); // { value: 1, done: false }
it.throw( "e1" ); // e1
// { value: 2, done: false }
it.next(); // foo: e2
// { value: undefined, done: true }
}
catch (err) {
console.log( "never gets here" );
}
it.next(); // { value: undefined, done: true }
当*foo()
调用yield 1
时,值1
原封不动地穿过了*bar()
,就像我们已经看到过的那样。
但这个代码段最有趣的部分是,当*foo()
调用throw "foo: e2"
时,这个错误传播到了*bar()
并立即被*bar()
的try..catch
块儿捕获。错误没有像值1
那样穿过*bar()
。
然后*bar()
的catch
将err
普通地输出("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非常简单:
function *foo() {
var x = yield 42;
console.log( x );
}
开始之前,我们将需要一个我们能够执行的称为foo()
的函数,它需要返回一个迭代器:
function foo() {
// ..
return {
next: function(v) {
// ..
}
// 我们将省略`return(..)`和`throw(..)`
};
}
现在,我们需要一些内部变量来持续跟踪我们的“generator”的逻辑走到了哪一个步骤。我们称它为state
。我们将有三种状态:起始状态的0
,等待完成yield
表达式的1
,和generator完成的2
。
每次next(..)
被调用时,我们需要处理下一个步骤,然后递增state
。为了方便,我们将每个步骤放在一个switch
语句的case
子句中,并且我们将它放在一个next(..)
可以调用的称为nextState(..)
的内部函数中。另外,因为x
是一个横跨整个“generator”作用域的变量,所以它需要存活在nextState(..)
函数的外部。
这是将它们放在一起(很明显,为了使概念的展示更清晰,它经过了某些简化):
function foo() {
function nextState(v) {
switch (state) {
case 0:
state++;
// `yield`表达式
return 42;
case 1:
state++;
// `yield`表达式完成了
x = v;
console.log( x );
// 隐含的`return`
return undefined;
// 无需处理状态`2`
}
}
var state = 0, x;
return {
next: function(v) {
var ret = nextState( v );
return { value: ret, done: (state == 2) };
}
// 我们将省略`return(..)`和`throw(..)`
};
}
最后,让我们测试一下我们的前ES6“generator”:
var it = foo();
it.next(); // { value: 42, done: false }
it.next( 10 ); // 10
// { value: undefined, done: true }
不赖吧?希望这个练习能在你的脑中巩固这个概念:generator实际上只是状态机逻辑的简单语法。这使它们可以广泛地应用。
Generator的使用
我们现在非常深入地理解了generator如何工作,那么,它们在什么地方有用?
我们已经看过了两种主要模式:
生产一系列值: 这种用法可以很简单(例如,随机字符串或者递增的数字),或者它也可以表达更加结构化的数据访问(例如,迭代一个数据库查询结果的所有行)。
这两种方式中,我们使用迭代器来控制generator,这样就可以为每次
next(..)
调用执行一些逻辑。在数据解构上的普通迭代器只不过生成值而没有任何控制逻辑。串行执行的任务队列: 这种用法经常用来表达一个算法中步骤的流程控制,其中每一步都要求从某些外部数据源取得数据。对每块儿数据的请求可能会立即满足,或者可能会异步延迟地满足。
从generator内部代码的角度来看,在
yield
的地方,同步或异步的细节是完全不透明的。另外,这些细节被有意地抽象出去,如此就不会让这样的实现细节把各个步骤间自然的,顺序的表达搞得模糊不清。抽象还意味着实现可以被替换/重构,而根本不用碰generator中的代码。
当根据这些用法观察generator时,它们的含义要比仅仅是手动状态机的一种不同或更好的语法多多了。它们是一种用于组织和控制有序地生产与消费数据的强大工具。