当前位置: 首页 > 工具软件 > diving > 使用案例 >

深入 Generator 函数(二) (Diving Deeper With ES6 Generators)

贺海
2023-12-01

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 函数基础。一旦你认为自己已经掌握了基础知识,那么我们现在就可以进一步了解一些更深入的细节了。

Error Handling

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)
}

Delegating(委托) Generators

你可能回发现自己想要做的另一件事使从你的 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 执行完成了的原因。

Summary

Generator 函数具有同步执行语义,这意味着你可以在 yield 语句中使用 try .. catch 错误处理机制来处理错误。Generator 迭代器还具有一个 throw() 方法,通过它可以在暂停的位置抛出一个错误,当然这个错误是可以在 generator 内部被捕获的。

yield * 允许你将当前 generator 函数的迭代器委派给另一个。结果是,yield * 可以作为双向传递,既可以传递消息,当然也可以传递错误。

但是,到目前为止,有一个根本问题仍然没有答复:generator 函数如何帮助我们使用异步代码模式呢?我们在目前的两篇文章中看到的都是 generator 函数的同步迭代。

关键是要构建这样一个机制 —— generator 暂停时启动异步任务,然后在异步任务结束时恢复执行(通过它的迭代器的 next() 方法)。我们将在下一篇文章中探讨如何使用 generator 来创建异步控制。敬请期待!

 类似资料: