深刻研究ES6 Generators

  ES6 Generators系列:

  1. ES6 Generators基本概念
  2. 深刻研究ES6 Generators
  3. ES6 Generators的异步应用
  4. ES6 Generators并发

  若是你还不知道什么是ES6 generators,请看个人前一篇文章“ES6 Generators基本概念” 。若是你已经对它有所了解,本文将带你深刻了解ES6 generators的一些细节。html

 

错误处理

  ES6 generators设计中最牛逼的部分之一就是generator函数内部的代码是同步的,即便在generator函数外部控制是异步进行的。并发

  也就是说,你可使用任何你所熟悉的错误处理机制来简单地在generator函数中处理错误,例如使用try..catch机制。异步

  来看一个例子:函数

function *foo() {
    try {
        var x = yield 3;
        console.log( "x: " + x ); // 有可能永远也不会运行到这儿!
    }
    catch (err) {
        console.log( "Error: " + err );
    }
}

  尽管函数会在yield 3表达式的位置暂停任意长的时间,可是若是有错误被发回generator函数,try..catch依然会捕获该错误!你能够尝试在异步回调中调用上面的代码。spa

  那么,如何才能将错误精准地发回给generator函数呢?设计

var it = foo();

var res = it.next(); // { value:3, done:false }

// 这里咱们不调用next(..)方法,而直接抛出一个异常:
it.throw( "Oops!" ); // Error: Oops!

  这里咱们使用了另外一个方法throw(..),它会在generator函数暂停的位置抛出一个错误,而后try..catch语句会捕获这个错误!code

  注意:若是你经过throw(..)方法向generator函数抛出一个错误,可是该generator函数中并无try..catch语句来捕获该错误,那么这个错误会被传回来(若是这个错误没有被其它代码捕获,则会被看成一个未处理的异常向上抛出)。因此:htm

function *foo() { }

var it = foo();
try {
    it.throw( "Oops!" );
}
catch (err) {
    console.log( "Error: " + err ); // Error: Oops!
}

  显然,反方向的错误处理也是可行的,看下面的代码:blog

function *foo() {
    var x = yield 3;
    var y = x.toUpperCase(); // 可能会引起类型错误!
    yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
    it.next( 42 ); // 42没有toUpperCase()方法
}
catch (err) {
    console.log( err ); // toUpperCase()引起TypeError错误
}

 

Generators委托

  你能够在一个generator函数体内调用另外一个generator函数,不是经过普通的方式实例化一个generator函数,其实是将当前generator函数的迭代控制委托给另外一个generator函数。咱们经过关键字yield *来实现。看下面的代码:文档

function *foo() {
    yield 3;
    yield 4;
}

function *bar() {
    yield 1;
    yield 2;
    yield *foo(); // yield *将当前函数的迭代控制委托给另外一个generator函数foo()
    yield 5;
}

for (var v of bar()) {
    console.log( v );
}
// 1 2 3 4 5

  注意这里咱们依然推荐yield *foo()这种写法,而不用yield* foo(),我在前一篇文章中也提到过这一点(推荐使用function *foo(){}而不用function* foo(){})。事实上,在不少其它的文章和文档中也都采用了前者,这种写法会让你的代码看起来更清晰一些。

  咱们来看一下上面代码的运行原理。在for..of循环遍历中,经过隐式调用next()方法将表达式yield 1yield 2的值返回,这一点咱们在前一篇文章中已经分析过了。在关键字yield *的位置,程序实例化并将迭代控制委托给另外一个generator函数foo()。一旦经过yield *将迭代控制从*bar()委托给*foo()(只是暂时性的),for..of循环将经过next()方法遍历foo(),所以表达式yield 3yield 4将对应的值返回给for..of循环。当对*foo()的遍历结束后,委托控制又从新回到以前的那个generator函数,因此表达式yield 5返回了对应的值。

  上面的代码很简单,只是经过yield表达式输出值。固然,你彻底能够不经过for..of循环而手动经过next(..)方法并传入相应的值来进行遍历,这些传入的值也会经过yield *关键字传递给对应的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

  虽然这里咱们只展现了一级委托,但理论上能够有任意多级委托,就是说上例中的generator函数*foo()中还能够有yield *表达式,从而将控制进一步委托给另外的generator函数,一级一级传递下去。

  还有一点就是yield *表达式容许接收被委托的generator函数的return返回值。

function *foo() {
    yield 2;
    yield 3;
    return "foo"; // 字符串"foo"会被返回给yield *表达式
}

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()表达式,程序将控制委托给generator函数*foo(),当函数foo()执行完毕后,经过return语句将值(字符串"foo")返回给yield *表达式,而后在bar()函数中,这个值最终被赋值给变量v

  Yieldyield *之间有个颇有趣的区别:在yield表达式中,接收的值是由随后的next(..)方法传入的参数,可是在yield *表达式中,它接收的是被委托的generator函数中return语句返回的值(此时经过next(..)方法将值传入的过程是透明的)。

  你也能够在yield *委托中进行双向错误处理:

function *foo() {
    try {
        yield 2;
    }
    catch (err) {
        console.log( "foo caught: " + err );
    }

    yield; // 暂停

    // 抛出一个错误
    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!" ); // 将会被foo()中的try..catch捕获
// foo caught: Uh oh!

it.next(); // { value:undefined, done:true }  --> 注意这里不会出现错误!
// bar caught: Oops!

  在上面的代码中,throw("Uh oh!")方法抛出一个错误,该错误被yield *委托的generator函数*foo()中的try..catch所捕获。一样地,* foo()中的throw "Oops!"语句将错误抛回给*bar(),而后被*bar()中的try..catch捕获。若是错误没有被捕获到,则会继续向上抛出。

 

总结

  从代码语义层面来看,generator函数是同步执行的,这意味着你能够在yield语句中使用try..catch来处理错误。另外,generator遍历器还有一个throw(..)方法,能够在其暂停的地方抛出一个错误,这个错误也能够被generator函数内部的try..catch捕获。

  关键字yield *容许你在当前的generator函数内部委托并遍历另外一个generator函数。咱们能够将参数经过yield *传入到被委托的generator函数体中,固然,错误信息也会经过yield *被传回来。

  到目前为止咱们还有一个最基本的问题没有回答,那就是如何在异步模式中使用generator函数。前面咱们看到的全部对generator函数的遍历都是同步执行的。

  关键是要构造一种机制,可以使generator函数在暂停的时候启动一个异步任务,而后在异步任务结束时恢复generator函数的执行(经过调用next()方法)。咱们将在下一篇文章中探讨在generator函数中建立这种异步控制的各类方法。敬请关注!

相关文章
相关标签/搜索