Diving Deeper With ES6 Generators
作者简介:Kyle Simpson is an Open Web Evangelist from Austin, TX, passionate about all things JavaScript. He's an author, workshop trainer, tech speaker, and OSS contributor/leader.
如果你仍然对 generator 函数不了解,请先阅读第一部分,Generator 函数基础。一旦你认为自己已经掌握了基础知识,那么我们现在就可以进一步了解一些更深入的细节了。
ES6 generator 函数设计中最强大的部分之一是即使外部迭代控制异步执行,generator 函数内的语义也是同步的。
这是一种奇怪/复杂的方式,表明你可以使用你可能非常熟悉的、简单的错误处理技术——即 try ... catch
机制。
举个例子:
function *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 呢?
var 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 函数暂停点发生一样。try .. catch
会像我们期望的那样捕获到这个错误!
注意:如果你将错误抛出到 generator 函数中,但没有 try .. catch
捕获它,错误将(如正常一样)传递回来(如果没有被捕获,最终会被作为未处理的错误而拒绝)。所以:
function *foo() { }
var it = foo();
try {
it.throw( "Oops!" );
}
catch (err) {
console.log( "Error: " + err ); // Error: Oops!
}
显然,错误处理的相反方向也起作用:
function *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)
}
你可能回发现自己想要做的另一件事使从你的 generator 内部调用另一个 generator。我不会以正常的方式实例化一个 generator,而是将自己的迭代控制委托给另一个 generator。为此,我们使用 yield
关键字的变体:yield *
("yield star")。
栗子:
function *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
正如第 1 部分(我使用 function *foo(){}
而不是 function* foo(){})中所解释的那样,我也使用 yield *foo()
而不是像其他文章/文档那样使用 yield* foo()
。我认为这样更准确/清楚,以说明发生了什么。
让我们分解一下这是如何运作的。yield 1
和 yield 2
将它们的值直接发送到 next()
的循环(隐藏)调用中,正如我们已经理解和期望的那样。
但是,遇到 yield *
,你会注意到,我们 yield
另一个 generator (通过 foo() 实例化的)。所以,我们从基础上对另一个 generator 的迭代器进行了 yield/delegating(委托)--这可能是最准确的思考方式了。
一旦 yield *
从 *bar()
中委托到 *foo()
,现在 for .. of
循环的 next()
调用实际上使控制的 foo(),因此 yield 3 和 yield 4 直接发送它们的值到 for .. of
循环。
一旦 *foo()
完成,控制就会返回到原来的 generator,最后调用 yield 5。
为了简单起见,此示例仅产生值。但是,如果你不使用 for .. of
循环,只需手动调用迭代器的 next() 函数,并传递消息,这些消息将以相同的预期方式通过 yield *
委托:
function *foo() {
var z = yield 3;
var w = yield 4;
console.log( "z: " + z + ", w: " + w );
}
function *bar() {
var x = yield 1;
var y = yield 2;
yield *foo(); // `yield*` delegates iteration control to `foo()`
var v = yield 5;
console.log( "x: " + x + ", y: " + y + ", v: " + v );
}
var it = bar();
it.next(); // { value:1, done:false }
it.next( "X" ); // { value:2, done:false }
it.next( "Y" ); // { value:3, done:false }
it.next( "Z" ); // { value:4, done:false }
it.next( "W" ); // { value:5, done:false }
// z: Z, w: W
it.next( "V" ); // { value:undefined, done:true }
// x: X, y: Y, v: V
虽然我们这里只展示了一层的代理(委托),但是没有理由为什么 *foo()
不能将 yield * 委托给另一个 generator 迭代器,然后再委托给另一个,等等。
yield *
可以做的另一个“技巧”就是从委托的 generator 中接收返回值。
function *foo() {
yield 2;
yield 3;
return "foo"; // return value back to `yield*` expression
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // "v: foo" { value:4, done:false }
it.next(); // { value:undefined, done:true }
正如你所看到的,yield *foo()
委托的迭代器有 next() 调用,直到它完成了。一旦它完成,foo() 就会 return
返回值(这个例子中是字符串“foo”),返回值被赋值给 yield *
表达式,然后赋值给本地变量 v。
yield
和 yield *
之间一个有趣的区别是:使用 yield
表达式,结果有随后的 next() 传入,但是使用 yield *
表达式,它只接收委托的 generator 的 return 的返回值(因为 next() 会通过委托直接传递值)。
你还可以在 yield * 委托中进行两个方向上的错误处理:
function *foo() {
try {
yield 2;
}
catch (err) {
console.log( "foo caught: " + err );
}
yield; // pause
// now, throw another error
throw "Oops!";
}
function *bar() {
yield 1;
try {
yield *foo();
}
catch (err) {
console.log( "bar caught: " + err );
}
}
var it = bar();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.throw( "Uh oh!" ); // will be caught inside `foo()`
// foo caught: Uh oh!
it.next(); // { value:undefined, done:true } --> No error here!
// bar caught: Oops!
正如你所看到的, throw("Uh oh!")
抛出的错误通过 yield *
委托到了 *foo()
,由它内部的 try .. catch
捕获了。同样的,在*foo()
内的 throw "Oops!"
抛回到了 *bar()
,然后被 *bar()
的 try .. catch
捕获。如果我们没有捕获它们中的任何一个,错误就会像你期望的那样继续传播出去。
注意:这里需要强调的是(原文中这里没有介绍) it.throw() 方法是 Generator.prototype.throw(),不是全局的 throw 方法,而且,throw 方法被捕获之后,会附带执行下一条 yield 表达式,也就是说,会附带执行一次 next() 方法。这也就是为什么上面 3 个 next() 方法就将 整个 generator 执行完成了的原因。
Generator 函数具有同步执行语义,这意味着你可以在 yield 语句中使用 try .. catch 错误处理机制来处理错误。Generator 迭代器还具有一个 throw() 方法,通过它可以在暂停的位置抛出一个错误,当然这个错误是可以在 generator 内部被捕获的。
yield * 允许你将当前 generator 函数的迭代器委派给另一个。结果是,yield * 可以作为双向传递,既可以传递消息,当然也可以传递错误。
但是,到目前为止,有一个根本问题仍然没有答复:generator 函数如何帮助我们使用异步代码模式呢?我们在目前的两篇文章中看到的都是 generator 函数的同步迭代。
关键是要构建这样一个机制 —— generator 暂停时启动异步任务,然后在异步任务结束时恢复执行(通过它的迭代器的 next() 方法)。我们将在下一篇文章中探讨如何使用 generator 来创建异步控制。敬请期待!