本文根据 https://davidwalsh.name/es6-generators-dive 翻译
如果你还不熟悉ES6 generator, 首先需要阅读并且玩下“Part1:ES6 Generators基础”的代码。一旦你已经掌握了基础部分,那么你就可以深入更多的细节了。
错误处理
ES6 generator设计中最强大部分之一是generator的代码语义是同步的,即便外部迭代控制异步执行。
这是一种很奇妙/复杂的说法,就是你可以使用简单的错误处理技术,你可能非常熟悉 —— 叫做try..catch
机制。
例如:1
2
3
4
5
6
7
8
9function *foo() {
try {
var x = yield 3;
console.log( "x: " + x ); // may never get here!
}
catch (err) {
console.log( "Error: " + err );
}
}
即便函数将暂停在yield 3
表达式,并且可能暂停了一段时间,如果一个错误被发送回generator, try..catch
都能够捕获它!试试在正常的异步回调中做同样的事情。:)
但是,如何准确的将一个错误发送回generator?1
2
3
4
5
6
7var it = foo();
var res = it.next(); // { value:3, done:false }
// instead of resuming normally with another `next(..)` call,
// let's throw a wrench (an error) into the gears:
it.throw( "Oops!" ); // Error: Oops!
在这里,你可以看到我们使用了迭代器上的另外一个方法 —— throw(...)
—— 它抛出一个错误给generator,在错误发生的个时间点,这时候generator正被yield-paused暂停。try..catch如你预期的一样捕获了这个错误。
注意,如果你 throw(..) 一个错误给generator, 但是没有try..catch捕获它,这个错误将会(如同正常的表现)传播出去(如果没有最终处理,那么会变成一个未处理的拒绝), 所以:
1 | function *foo() { } |
显然,错误处理的反过来也同样有效:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function *foo() {
var x = yield 3;
var y = x.toUpperCase(); // could be a TypeError error!
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next( 42 ); // `42` won't have `toUpperCase()`
}
catch (err) {
console.log( err ); // TypeError (from `toUpperCase()` call)
}
代理Generators
你可能发现你想要做的另外一件事情是在另外一个generator里面调用你的generator函数。我的意思不只是在正常的方法中初始化一个generator,而是实质上的把你的迭代器控制委托给另外一个generator。 为了实现这样的需求,我们使用yield关键字的另外一种方式:yield *
(“yield star”).
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function *foo() {
yield 3;
yield 4;
}
function *bar() {
yield 1;
yield 2;
yield *foo(); // `yield *` delegates iteration control to `foo()`
yield 5;
}
for (var v of bar()) {
console.log( v );
}
// 1 2 3 4 5
跟第一部分描述的类似(我使用function *foo() { }
来代替function* foo() { }
), 我也使用yield *foo()
来取代其他文章/文档中说的yield* foo()
。我认为这样更能准确/清晰说明将发生什么。
让我们来破解这是如何工作的。yield 1
和yield 2
把他们的值直接传递给了for..of
循环的next()
调用(隐式的), 想我们了解和期望的那样。
但是之后遇到了yield *
, 并且你将注意到我们赋予了它另外一个generator,通初始化(foo()
)。也就是我们委托了另外一个generator的跌代器 —— 这可能是我们描述它的最准确的方法。
一旦yield*
被从*bar()
代理(临时的)成*foo()
, 现在for..of
循环的next()调用实际上是对foo()
的控制,也就是yield 3
和yield 4
会发送他们的值给for..of
循环。
一旦*foo()
完成,控制权返回给原来的generator, 它最后调用了yield 5
。
为了简单起见,这个例子只传出值。但是当然,如果你不使用for..of
循环,而只是人工调用迭代器的next(..)
方法并且传递了信息进去,这些信息将会通过yield*
代理进去,如期望的一样:
1 | function *foo() { |
虽然我们这里只是显示了代理的一个层级,没有理由说*foo()
不能通过yield*
代理另外一个generator迭代器,然后这个generator又代理另外一个,等等。
另外一个“特性”是yield*
可以从被代理的那个generator那回收值。
1 | function *foo() { |
如你所见,yield *foo()
被委托给迭代控制器(next()
调用)直到它完成,然后一旦他完成,从foo()
返回的值(在这里是字符串”foo”)被作为结果值设置给yield*
表达式, 然后被赋值给本地变量v.
这是一个yield
和yield*
之间有趣的区别: yield
表达式,结果总是随后的next(..)
调用中传递的值,而yield*
表达式, 它只从代理的generator的返回值中获取结果(因为next(..)
通过代理透明的传值)。
你也可以通过yield *
代理双向进行错误处理(见上文):
1 | function *foo() { |
如你所见,throw("Uh oh!")
通过yield*
代理抛出错误给*foo()
的try..catch
。同样的,在*foo()
内部抛出“Oops!”抛给了外部的*bar()
,它通过另外一个try..catch
捕获了错误。 如果我们没有捕获他们中的任意一个,错误都将如你预期的那样继续往外传播。
总结
Generators有同步执行语义,意味着你可以在yield
语句中使用try..catch
这种错误处理机制。generator迭代器还拥有throw(..)
的方法可以在generator暂停的位置抛出一个错误给generator,当然它是可以被generator里面的try..catch
捕获的。
yield*
允许你委托迭代控制从当前的generator到另外一个generator。结果是yield*
表现的如同一个双方向的传递,无论是消息还是错误。
但是,到目前为止有个根本性的问题还没有答案:generators如何在异步代码模式中起作用?在这两篇文章中,我们所看到的都是同步迭代的generator函数。
问题的关键是建立一个机制,在generator停下来的时候开始一个异步任务,然后当异步任务结束的时候恢复(通过他的迭代器的next()
调用)。我们将在下一篇文章中探讨通过generators来创建异步控制的不同方法。敬请关注!