Generator 委托
在上一节中,我们展示了从generator内部调用普通函数,和它如何作为一种有用的技术来将实现细节(比如异步Promise流程)抽象出去。但是为这样的任务使用普通函数的缺陷是,它必须按照普通函数的规则行动,也就是说它不能像generator那样用yield
来暂停自己。
在你身上可能发生这样的事情:你可能会试着使用我们的run(..)
帮助函数,从一个generator中调用另个一generator。比如:
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// 通过`run(..)`“委托”到`*foo()`
var r3 = yield run( foo );
console.log( r3 );
}
run( bar );
通过再一次使用我们的run(..)
工具,我们在*bar()
内部运行*foo()
。我们利用了这样一个事实:我们早先定义的run(..)
返回一个promise,这个promise在generator运行至完成时才解析(或发生错误),所以如果我们从一个run(..)
调用中yield
出一个promise给另一个run(..)
,它就会自动暂停*bar()
直到*foo()
完成。
但这里有一个更好的办法将*foo()
调用整合进*bar()
,它称为yield
委托。yield
委托的特殊语法是:yield * __
(注意额外的*
)。让它在我们前面的例子中工作之前,让我们看一个更简单的场景:
function *foo() {
console.log( "`*foo()` starting" );
yield 3;
yield 4;
console.log( "`*foo()` finished" );
}
function *bar() {
yield 1;
yield 2;
yield *foo(); // `yield`-delegation!
yield 5;
}
var it = bar();
it.next().value; // 1
it.next().value; // 2
it.next().value; // `*foo()` starting
// 3
it.next().value; // 4
it.next().value; // `*foo()` finished
// 5
注意: 在本章早前的一个注意点中,我解释了为什么我偏好function *foo() ..
而不是function* foo() ..
,相似地,我也偏好——与关于这个话题的其他大多数文档不同——说yield *foo()
而不是yield* foo()
。*
的摆放是纯粹的风格问题,而且要看你的最佳判断。但我发现保持统一风格很吸引人。
yield *foo()
委托是如何工作的?
首先,正如我们看到过的那样,调用foo()
创建了一个 迭代器。然后,yield *
将(当前*bar()
generator的) 迭代器 的控制委托/传递给这另一个*foo()
迭代器。
那么,前两个it.next()
调用控制着*bar()
,但当我们发起第三个it.next()
调用时,*foo()
就启动了,而且这时我们控制的是*foo()
而非*bar()
。这就是为什么它称为委托——*bar()
将它的迭代控制委托给*foo()
。
只要it
迭代器 的控制耗尽了整个*foo()
迭代器,它就会自动地将控制返回到*bar()
。
那么现在回到前面的三个顺序Ajax请求的例子:
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// 通过`run(..)`“委托”到`*foo()`
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
这个代码段和前面使用的版本的唯一区别是,使用了yield *foo()
而不是前面的yield run(foo)
。
注意: yield *
让出了迭代控制,不是generator控制;当你调用*foo()
generator时,你就yield
委托给它的 迭代器。但你实际上可以yield
委托给任何 迭代器;yield *[1,2,3]
将会消费默认的[1,2,3]
数组值 迭代器。
为什么委托?
yield
委托的目的很大程度上是为了代码组织,而且这种方式是与普通函数调用对称的。
想象两个分别提供了foo()
和bar()
方法的模块,其中bar()
调用foo()
。它们俩分开的原因一般是由于为了程序将它们作为分离的程序来调用而进行的恰当组织。例如,可能会有一些情况foo()
需要被独立调用,而其他地方bar()
来调用foo()
。
由于这些完全相同的原因,将generator分开可以增强程序的可读性,可维护性,与可调试性。从这个角度讲,yield *
是一种快捷的语法,用来在*bar()
内部手动地迭代*foo()
的步骤。
如果*foo()
中的步骤是异步的,这样的手动方式可能会特别复杂,这就是为什么你可能会需要那个run(..)
工具来做它。正如我们已经展示的,yield *foo()
消灭了使用run(..)
工具的子实例(比如run(foo)
)的需要。
委托消息
你可能想知道,这种yield
委托在除了与 迭代器 控制一起工作以外,是如何与双向消息传递一起工作的。仔细查看下面这些通过yield
委托进进出出的消息流:
function *foo() {
console.log( "inside `*foo()`:", yield "B" );
console.log( "inside `*foo()`:", yield "C" );
return "D";
}
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-委托!
console.log( "inside `*bar()`:", yield *foo() );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// inside `*foo()`: 3
// inside `*bar()`: D
// outside: E
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: 4
// outside: F
特别注意一下it.next(3)
调用之后的处理步骤:
- 值
3
被传入(通过*bar
里的yield
委托)在*foo()
内部等待中的yield "C"
表达式。 - 然后
*foo()
调用return "D"
,但是这个值不会一路返回到外面的it.next(3)
调用。 - 相反地,值
"D"
作为结果被发送到在*bar()
内部等待中的yield *foo()
表示式——这个yield
委托表达式实质上在*foo()
被耗尽之前一直被暂停着。所以"D"
被送到*bar()
内部来让它打印。 yield "E"
在*bar()
内部被调用,而且值"E"
被让出到外部作为it.next(3)
调用的结果。
从外部 迭代器(it
)的角度来看,在初始的generator和被委托的generator之间的控制没有任何区别。
事实上,yield
委托甚至不必指向另一个generator;它可以仅被指向一个非generator的,一般的 iterable。比如:
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-委托至一个非generator
console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// outside: C
console.log( "outside:", it.next( 3 ).value );
// outside: D
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: undefined
// outside: E
console.log( "outside:", it.next( 5 ).value );
// inside `*bar()`: 5
// outside: F
注意这个例子与前一个之间,被接收/报告的消息的不同之处。
最惊人的是,默认的array
迭代器 不关心任何通过next(..)
调用被发送的消息,所以值2
,3
,与4
实质上被忽略了。另外,因为这个 迭代器 没有明确的return
值(不像前面使用的*foo()
),所以yield *
表达式在它完成时得到一个undefined
。
异常也委托!
与yield
委托在两个方向上透明地传递消息的方式相同,错误/异常也在双向传递:
function *foo() {
try {
yield "B";
}
catch (err) {
console.log( "error caught inside `*foo()`:", err );
}
yield "C";
throw "D";
}
function *bar() {
yield "A";
try {
yield *foo();
}
catch (err) {
console.log( "error caught inside `*bar()`:", err );
}
yield "E";
yield *baz();
// note: can't get here!
yield "G";
}
function *baz() {
throw "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// outside: B
console.log( "outside:", it.throw( 2 ).value );
// error caught inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// error caught inside `*bar()`: D
// outside: E
try {
console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
console.log( "error caught outside:", err );
}
// error caught outside: F
在这段代码中有一些事情要注意:
- 但我们调用
it.throw(2)
时,它发送一个错误消息2
到*bar()
,而*bar()
将它委托至*foo()
,然后*foo()
来catch
它并平静地处理。之后,yield "C"
把"C"
作为返回的value
发送回it.throw(2)
调用。 - 接下来值
"D"
被从*foo()
内部throw
出来并传播到*bar()
,*bar()
会catch
它并平静地处理。然后yield "E"
把"E"
作为返回的value
发送回it.next(3)
调用。 - 接下来,一个异常从
*baz()
中throw
出来,而没有被*bar()
捕获——我们没在外面catch
它——所以*baz()
和*bar()
都被设置为完成状态。这段代码结束后,即便有后续的next(..)
调用,你也不会得到值"G"
——它们的value
将返回undefined
。
异步委托
最后让我们回到早先的多个顺序Ajax请求的例子,使用yield
委托:
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
在*bar()
内部,与调用yield run(foo)
不同的是,我们调用yield *foo()
就可以了。
在前一个版本的这个例子中,Promise机制(通过run(..)
控制的)被用于将值从*foo()
中的return r3
传送到*bar()
内部的本地变量r3
。现在,这个值通过yield *
机制直接返回。
除此以外,它们的行为是一样的。
“递归”委托
当然,yield
委托可以一直持续委托下去,你想连接多少步骤就连接多少。你甚至可以在具有异步能力的generator上“递归”使用yield
委托——一个yield
委托至自己的generator:
function *foo(val) {
if (val > 1) {
// 递归委托
val = yield *foo( val - 1 );
}
return yield request( "http://some.url/?v=" + val );
}
function *bar() {
var r1 = yield *foo( 3 );
console.log( r1 );
}
run( bar );
注意: 我们的run(..)
工具本可以用run( foo, 3 )
来调用,因为它支持用额外传递的参数来进行generator的初始化。然而,为了在这里高调展示yield *
的灵活性,我们使用了无参数的*bar()
。
这段代码之后的处理步骤是什么?坚持住,它的细节要描述起来可是十分错综复杂:
run(bar)
启动了*bar()
generator。foo(3)
为*foo(..)
创建了 迭代器 并传递3
作为它的val
参数。- 因为
3 > 1
,foo(2)
创建了另一个 迭代器 并传递2
作为它的val
参数。 - 因为
2 > 1
,foo(1)
又创建了另一个 迭代器 并传递1
作为它的val
参数。 1 > 1
是false
,所以我们接下来用值1
调用request(..)
,并得到一个代表第一个Ajax调用的promise。- 这个promise被
yield
出来,回到*foo(2)
generator实例。 yield *
将这个promise传出并回到*foo(3)
生成generator。另一个yield *
把这个promise传出到*bar()
generator实例。而又有另一个yield *
把这个promise传出到run(..)
工具,而它将会等待这个promise(第一个Ajax请求)再处理。- 当这个promise解析时,它的完成消息会被发送以继续
*bar()
,*bar()
通过yield *
把消息传递进*foo(3)
实例,*foo(3)
实例通过yield *
把消息传递进*foo(2)
generator实例,*foo(2)
实例通过yield *
把消息传给那个在*foo(3)
generator实例中等待的一般的yield
。 - 这第一个Ajax调用的应答现在立即从
*foo(3)
generator实例中被return
,作为*foo(2)
实例中yield *
表达式的结果发送回来,并赋值给本地val
变量。 *foo(2)
内部,第二个Ajax请求用request(..)
发起,它的promise被yield
回到*foo(1)
实例,然后一路yield *
传播到run(..)
(回到第7步)。当promise解析时,第二个Ajax应答一路传播回到*foo(2)
generator实例,并赋值到他本地的val
变量。- 最终,第三个Ajax请求用
request(..)
发起,它的promise走出到run(..)
,然后它的解析值一路返回,最后被return
到在*bar()
中等待的yield *
表达式。
天!许多疯狂的头脑杂技,对吧?你可能想要把它通读几遍,然后抓点儿零食放松一下大脑!