深刻研究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函數中建立這種異步控制的各類方法。敬請關注!

相關文章
相關標籤/搜索