本文翻譯自:Diving Deeper With ES6 Generators
由於個人能力有限,翻譯中難免有紕漏和錯誤,望不吝指正issue
ES6 Generators:完整系列
如果你依然對ES6 generators不是很熟悉,建議你閱讀本系列第一篇文章“第一部分:ES6 Generators基礎指南”,並練習其中的程式碼片段。一旦你覺得對基礎部分掌握透徹了,那我們就可以開始深入理解Generator函式的一些細節部分。
錯誤處理
ES6 generators設計中最為強大部分莫過於從語義上理解generator中的程式碼都是同步的,儘管外部的迭代控制器是非同步執行的。
也就是說,你可以使用簡單的錯誤處理技術來對generators函式進行容錯處理, 也就是你最為熟悉的try...catch
機制。
例如:
function *foo() {
try {
var x = yield 3;
console.log( "x: " + x ); // may never get here!
}
catch (err) {
console.log( "Error: " + err );
}
}
儘管上面例子中的foo
generator函式會在yield 3
表示式後暫停執行,並且可能暫停任意長的時間,如果向generator函式內部傳入一個錯誤,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!
如上程式碼,你會看到iterator的另外一個方法- –throw(..)
– -,該方法向generator函式內部傳入一個錯誤,該錯誤就如同在generator函式內部暫停執行的yield
語句處丟擲的錯誤一樣,正如你所願,try...catch
模組捕獲了通過throw
方法丟擲的錯誤。
注意:如果你通過throw(..)
方法向generator函式內部丟擲一個錯誤,同時在函式內部又沒有try...catch
模組來捕獲錯誤,該錯誤(如同正常的錯誤冒泡機制)將從generator函式冒泡到函式外部(如果始終都沒對該錯誤進行處理,該錯誤將冒泡到最外層成為未捕獲錯誤)。程式碼如下:
function *foo() { }
var it = foo();
try {
it.throw( "Oops!" );
}
catch (err) {
console.log( "Error: " + err ); // Error: Oops!
}
顯而易見,反向的錯誤處理依然能夠正常工作(譯者注:generator函式內部丟擲錯誤,在generator外部捕獲):
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)
}
代理 Generators函式
在使用generator函式的過程中,另外一件你可能想要做的事就是在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
在第一篇文章中已經提及(在第一篇文章中,我使用function *foo() { }
的語法格式,而不是function* foo() { }
),在這裡,我們依然使用yield *foo()
,而不是yield* foo()
,儘管很多文章/文件喜歡採用後面一種語法格式。我認為前面一種語法格式更加準確/清晰得表達此語法含義。
讓我們來分解上面程式碼是如何工作的。yield 1
和yield 2
表示式直接將值通過for..of
迴圈(隱式)呼叫next()
傳遞到外部,正如我們已經理解並期待的那樣。
在程式碼執行過程中,我們遇到了yield *
表示式,你將看到我們通過執行foo()
將控制權交給了另外一個generator函式。因此我們基本上就是出產/委託給了另外一個generator函式的迭代器- -也許這就是最準確的理解代理generator函式如何工作的。
一旦yield *
表示式(臨時的)在*bar()
函式中將控制權委託給*foo()
函式,那麼現在for..of
迴圈中的next()
方法的執行將完全控制foo()
,因此yield 3
和yield 4
表示式將他們的值通過for..of
迴圈返回到外部。
當*foo()
執行結束,控制權重新交回最初的generator函式,最後在外層bar
函式中執行yield 5
。
簡單起見,在上面的例項中,我們僅通過yield
表示式將值傳遞到generator函式外部,當然,如果我們不用for..of
迴圈,而是手動的執行迭代器的next()
方法來向函式內部傳遞值,這些值也會按你所期待的方式傳遞給通過yield *
代理的generator函式中:
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迭代器,甚至繼續巢狀代理其他generator函式,等等。
yield *
表示式可以實現另外一個竅門,就是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()
方法顯式的將值傳遞到被代理的generator函式中)。
你依然可以雙向的對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()
函式中被另外一個try..catch
模組捕獲,如果我們沒有捕獲到其中的某一條錯誤,該錯誤將會按你所期待的方式繼續向上冒泡。
總結
Generators函式擁有同步執行的語義,這也意味著你可以通過try..catch
錯誤處理機制來橫跨yield
語句進行錯誤處理。同時,generator迭代器有一個throw()
方法來向generator函式中暫停處丟擲一個錯誤,該錯誤依然可以通過generator函式內部的try..catch
模組進行捕獲處理。
yield *
關鍵字允許你將迭代控制權從當前generator函式委託給其他generator函式。結果就是,yield *
將扮演一個雙向的資訊和錯誤傳遞角色。
但是到目前為止,一個基礎的問題依然沒有解決:generator函式怎麼幫助我們處理非同步模式?在以上兩篇文章中我們一直討論generator函式的同步迭代模式。
構想generator函式非同步機制的關鍵點在於,通過generator函式的暫停執行來開始一個非同步任務,然後通過generator函式的重新啟動(通過迭代器的next()
方法的執行)來結束上面的非同步任務。我們可以在接下來的文章中發現generator函式形式各樣的非同步控制機制。近期期待!