# 第四章: Generator
在第二章中,我們發現了在使用回調表達異步流程控制時的兩個關鍵缺陷:
* 基于回調的異步與我們的大腦規劃任務的各個步驟的過程不相符。
* 由于?*控制倒轉*?回調是不可靠的,也是不可組合的。
在第三章中,我們詳細地討論了Promise如何反轉回調的?*控制倒轉*,重建了可靠性/可組合性。
現在讓我們把注意力集中到用一種順序的,看起來同步的風格來表達異步流程控制。使這一切成為可能的“魔法”是ES6的?generator。
## 打破運行至完成
在第一章中,我們講解了一個JS開發者們在他們的代碼中幾乎永恒依仗的一個認識:一旦函數開始執行,它將運行直至完成,沒有其他的代碼可以在運行期間干擾它。
這看起來可能很滑稽,ES6引入了一種新型的函數,它不按照“運行至完成”的行為進行動作。這種新型的函數稱為“generator(生成器)”。
為了理解它的含義,讓我們看看這個例子:
```source-js
var x = 1;
function foo() {
x++;
bar(); // <-- 這一行會發生什么?
console.log( "x:", x );
}
function bar() {
x++;
}
foo(); // x: 3
```
在這個例子中,我們確信`bar()`會在`x++`和`console.log(x)`之間運行。但如果`bar()`不在這里呢?很明顯結果將是`2`而不是`3`。
現在讓我們來燃燒你的大腦。要是`bar()`不存在,但以某種方式依然可以在`x++`和`console.log(x)`語句之間運行呢?這可能嗎?
在?搶占式(preemptive)?多線程語言中,`bar()`去“干擾”并正好在兩個語句之間那一時刻運行,實質上時可能的。但JS不是搶占式的,也(還)不是多線程的。但是,如果`foo()`本身可以用某種辦法在代碼的這一部分指示一個“暫停”,那么這種“干擾”(并發)的?協作?形式就是可能的。
注意:?我使用“協作”這個詞,不僅是因為它與經典的并發術語有關聯(見第一章),也因為正如你將在下一個代碼段中看到的,ES6在代碼中指示暫停點的語法是`yield`——暗示一個讓出控制權的禮貌的?*協作*。
這就是實現這種協作并發的ES6代碼:
```source-js
var x = 1;
function *foo() {
x++;
yield; // 暫停!
console.log( "x:", x );
}
function bar() {
x++;
}
```
注意:?你將很可能在大多數其他的JS文檔/代碼中看到,一個generator的聲明被格式化為`function* foo() { .. }`而不是我在這里使用的`function *foo() { .. }`——唯一的區別是擺放`*`位置的風格。這兩種形式在功能性/語法上是完全一樣的,還有第三種`function*foo() { .. }`(沒空格)形式。這兩種風格存在爭議,但我基本上偏好`function *foo..`,因為當我在寫作中用`*foo()`引用一個generator時,這種形式可以匹配我寫的東西。如果我只說`foo()`,你就不會清楚地知道我是在說一個generator還是一個一般的函數。這純粹是一個風格偏好的問題。
現在,我們該如何運行上面的代碼,使`bar()`在`yield`那一點取代`*foo()`的執行?
```source-js
// 構建一個迭代器`it`來控制generator
var it = foo();
// 在這里開始`foo()`!
it.next();
x; // 2
bar();
x; // 3
it.next(); // x: 3
```
好了,這兩段代碼中有不少新的,可能使人困惑的東西,所以我們得跋涉好一段了。在我們用ES6的generator來講解不同的機制/語法之前,讓我們過一遍這個行為的流程:
1. `it = foo()`操作?*不會*?執行`*foo()`generator,它只不過構建了一個用來控制它執行的?*迭代器(iterator)*。我們一會更多地討論?*迭代器*。
2. 第一個`it.next()`啟動了`*foo()`generator,并且運行`*foo()`第一行上的`x++`。
3. `*foo()`在`yield`語句處暫停,就在這時第一個`it.next()`調用結束。在這個時刻,`*foo()`依然運行而且是活動的,但是處于暫停狀態。
4. 我們觀察`x`的值,現在它是`2`.
5. 我們調用`bar()`,它再一次用`x++`遞增`x`。
6. 我們再一次觀察`x`的值,現在它是`3`。
7. 最后的`it.next()`調用使`*foo()`generator從它暫停的地方繼續運行,而后運行使用`x`的當前值`3`的`console.log(..)`語句。
清楚的是,`*foo()`啟動了,但?*沒有*?運行到底——它停在`yield`。我們稍后繼續`*foo()`,讓它完成,但這甚至不是必須的。
所以,一個generator是一種函數,它可以開始和停止一次或多次,甚至沒必要一定要完成。雖然為什么它很強大看起來不那么明顯,但正如我們將要在本章剩下的部分將要講到的,它是我們用于在我們的代碼中構建“generator異步流程控制”模式的基礎構建塊兒之一。
### 輸入和輸出
一個generator函數是一種帶有我們剛才提到的新型處理模型的函數。但它仍然是一個函數,這意味著依舊有一些不變的基本原則——即,它依然接收參數(也就是“輸入”),而且它依然返回一個值(也就是“輸出”):
```source-js
function *foo(x,y) {
return x * y;
}
var it = foo( 6, 7 );
var res = it.next();
res.value; // 42
```
我們將`6`和`7`分別作為參數`x`和`y`傳遞給`*foo(..)`。而`*foo(..)`將值`42`返回給調用端代碼。
現在我們可以看到發生器的調用和一般函數的調用的一個不同之處了。`foo(6,7)`顯然看起來很熟悉。但微妙的是,`*foo(..)`generator不會像一個函數那樣實際運行起來。
相反,我們只是創建了?*迭代器*?對象,將它賦值給變量`it`,來控制`*foo(..)`generator。當我們調用`it.next()`時,它指示`*foo(..)`generator從現在的位置向前推進,直到下一個`yield`或者generator的最后。
`next(..)`調用的結果是一個帶有`value`屬性的對象,它持有從`*foo(..)`返回的任何值(如果有的話)。換句話說,`yield`導致在generator運行期間,一個值被從中發送出來,有點兒像一個中間的`return`。
但是,為什么我們需要這個完全間接的?*迭代器*?對象來控制generator還不清楚。我們回頭會討論它的,我保證。
#### 迭代通信
generator除了接收參數和擁有返回值,它們還內建有更強大,更吸引人的輸入/輸出消息能力,這是通過使用`yield`和`next(..)`實現的。
考慮下面的代碼:
```source-js
function *foo(x) {
var y = x * (yield);
return y;
}
var it = foo( 6 );
// 開始`foo(..)`
it.next();
var res = it.next( 7 );
res.value; // 42
```
首先,我們將`6`作為參數`x`傳入。之后我們調用`it.next()`,它啟動了`*foo(..)`.
在`*foo(..)`內部,`var y = x ..`語句開始被處理,但它運行到了一個`yield`表達式。就在這時,它暫停了`*foo(..)`(就在賦值語句的中間!),而且請求調用端代碼為`yield`表達式提供一個結果值。接下來,我們調用`it.next(7)`,將`7`這個值傳回去作為暫停的`yield`表達式的結果。
所以,在這個時候,賦值語句實質上是`var y = 6 * 7`。現在,`return y`將值`42`作為結果返回給`it.next( 7 )`調用。
注意一個非常重要,而且即便是對于老練的JS開發者也非常容易犯糊涂的事情:根據你的角度,在`yield`和`next(..)`調用之間存在著錯位。一般來說,你所擁有的`next(..)`調用的數量,會比你所擁有的`yield`語句的數量多一個——前面的代碼段中有一個`yield`和兩個`next(..)`調用。
為什么會有這樣的錯位?
因為第一個`next(..)`總是啟動一個generator,然后運行至第一個`yield`。但是第二個`next(..)`調用滿足了第一個暫停的`yield`表達式,而第三個`next(..)`將滿足第二個`yield`,如此反復。
##### 兩個疑問的故事
實際上,你主要考慮的是哪部分代碼會影響你是否感知到錯位。
僅考慮generator代碼:
```source-js
var y = x * (yield);
return y;
```
這?第一個?`yield`基本上是在?*問一個問題*:“我應該在這里插入什么值?”
誰來回答這個問題?好吧,第一個?`next()`在這個時候已經為了啟動generator而運行過了,所以很明顯?*它*?不能回答這個問題。所以,第二個?`next(..)`調用必須回答由?第一個?`yield`提出的問題。
看到錯位了吧——第二個對第一個?
但是讓我們反轉一下我們的角度。讓我們不從generator的角度看問題,而從迭代器的角度看。
為了恰當地描述這種角度,我們還需要解釋一下,消息可以雙向發送——`yield ..`作為表達式可以發送消息來應答`next(..)`調用,而`next(..)`可以發送值給暫停的`yield`表達式。考慮一下這段稍稍調整過的代碼:
```source-js
function *foo(x) {
var y = x * (yield "Hello"); // <-- 讓出一個值!
return y;
}
var it = foo( 6 );
var res = it.next(); // 第一個`next()`,不傳遞任何東西
res.value; // "Hello"
res = it.next( 7 ); // 傳遞`7`給等待中的`yield`
res.value; // 42
```
`yield ..`和`next(..)`一起成對地?在generator運行期間?構成了一個雙向消息傳遞系統。
那么,如果只看?*迭代器*?代碼:
```source-js
var res = it.next(); // 第一個`next()`,不傳遞任何東西
res.value; // "Hello"
res = it.next( 7 ); // 傳遞`7`給等待中的`yield`
res.value; // 42
```
注意:?我們沒有傳遞任何值給第一個`next()`調用,而且是故意的。只有一個暫停的`yield`才能接收這樣一個被`next(..)`傳遞的值,但是當我們調用第一個`next()`時,在generator的最開始并?沒有任何暫停的`yield`?可以接收這樣的值。語言規范和所有兼容此語言規范的瀏覽器只會無聲地?丟棄?任何傳入第一個`next()`的東西。傳遞這樣的值是一個壞主意,因為你只不過創建了一些令人困惑的無聲“失敗”的代碼。所以,記得總是用一個無參數的`next()`來啟動generator。
第一個`next()`調用(沒有任何參數的)基本上是在?*問一個問題*:“`*foo(..)`generator將要給我的?*下一個*?值是什么?”,誰來回答這個問題?第一個`yield`表達式。
看到了?這里沒有錯位。
根據你認為是?*誰*?在問問題,在`yield`和`next(..)`之間的錯位既存在又不存在。
但等一下!跟`yield`語句的數量比起來,還有一個額外的`next()`。那么,這個最后的`it.next(7)`調用又一次在詢問generator?*下一個*?產生的值是什么。但是沒有`yield`語句剩下可以回答了,不是嗎?那么誰來回答?
`return`語句回答這個問題!
而且如果在你的generator中?沒有`return`——比起一般的函數,generator中的`return`當然不再是必須的——總會有一個假定/隱式的`return;`(也就是`return undefined;`),它默認的目的就是回答由最后的`it.next(7)`調用?*提出*?的問題。
這些問題與回答——用`yield`和`next(..)`進行雙向消息傳遞——十分強大,但還是看不出來這些機制與異步流程控制有什么聯系。我們正在接近真相!
### 多迭代器
從語法使用上來看,當你用一個?*迭代器*?來控制generator時,你正在控制聲明的generator函數本身。但這里有一個容易忽視的微妙細節:每當你構建一個?*迭代器*,你都隱含地構建了一個將由這個?*迭代器*?控制的generator的實例。
你可以讓同一個generator的多個實例同時運行,它們甚至可以互動:
```source-js
function *foo() {
var x = yield 2;
z++;
var y = yield (x * z);
console.log( x, y, z );
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; // 2 <-- 讓出2
var val2 = it2.next().value; // 2 <-- 讓出2
val1 = it1.next( val2 * 10 ).value; // 40 <-- x:20, z:2
val2 = it2.next( val1 * 5 ).value; // 600 <-- x:200, z:3
it1.next( val2 / 2 ); // y:300
// 20 300 3
it2.next( val1 / 4 ); // y:10
// 200 10 3
```
警告:?同一個generator的多個并發運行實例的最常見的用法,不是這樣的互動,而是generator在沒有輸入的情況下,從一些連接著的獨立資源中產生它自己的值。我們將在下一節中更多地討論產生值。
讓我們簡單地走一遍這個處理過程:
1. 兩個`*foo()`在同時啟動,而且兩個`next()`都分別從`yield 2`語句中得到了`2`的`value`。
2. `val2 * 10`就是`2 * 10`,它被發送到第一個generator實例`it1`,所以`x`得到值`20`。`z`將`1`遞增至`2`,然后`20 * 2`被`yield`出來,將`val1`設置為`40`。
3. `val1 * 5`就是`40 * 5`,它被發送到第二個generator實例`it2`中,所以`x`得到值`200`。`z`又一次遞增,從`2`到`3`,然后`200 * 3`被`yield`出來,將`val2`設置為`600`。
4. `val2 / 2`就是`600 / 2`,它被發送到第一個generator實例`it1`,所以`y`得到值`300`,然后分別為它的`x y z`值打印出`20 300 3`。
5. `val1 / 4`就是`40 / 4`,它被發送到第一個generator實例`it2`,所以`y`得到值`10`,然后分別為它的`x y z`值打印出`200 10 3`。
這是在你腦海中跑過的一個“有趣”的例子。你還能保持清醒?
#### 穿插
回想第一章中“運行至完成”一節的這個場景:
```source-js
var a = 1;
var b = 2;
function foo() {
a++;
b = b * a;
a = b + 3;
}
function bar() {
b--;
a = 8 + b;
b = a * 2;
}
```
使用普通的JS函數,當然要么是`foo()`可以首先運行完成,要么是`bar()`可以首先運行至完成,但是`foo()`不可能與`bar()`穿插它的獨立語句。所以,前面這段代碼只有兩個可能的結果。
然而,使用generator,明確地穿插(甚至是在語句中間!)是可能的:
```source-js
var a = 1;
var b = 2;
function *foo() {
a++;
yield;
b = b * a;
a = (yield b) + 3;
}
function *bar() {
b--;
yield;
a = (yield 8) + b;
b = a * (yield 2);
}
```
根據?*迭代器*?控制`*foo()`與`*bar()`分別以什么樣的順序被調用,前面這段代碼可以產生幾種不同的結果。換句話說,通過兩個generator在同一個共享的變量上穿插,我們實際上可以展示(以一種模擬的方式)在第一章中討論的,理論上的“線程的競合狀態”環境。
首先,讓我們制造一個稱為`step(..)`的幫助函數,讓它控制?*迭代器*:
```source-js
function step(gen) {
var it = gen();
var last;
return function() {
// 不論`yield`出什么,只管在下一次時直接把它塞回去!
last = it.next( last ).value;
};
}
```
`step(..)`初始化一個generator來創建它的`it`?*迭代器*,然后它返回一個函數,每次這個函數被調用時,都將?*迭代器*?向前推一步。另外,前一個被`yield`出來的值將被直接發給下一步。所以,`yield 8`將變成`8`而`yield b`將成為`b`(不管它在`yield`時是什么值)。
現在,為了好玩兒,讓我們做一些實驗,來看看將這些`*foo()`與`*bar()`的不同塊兒穿插時的效果。我們從一個無聊的基本情況開始,保證`*foo()`在`*bar()`之前全部完成(就像我們在第一章中做的那樣):
```source-js
// 確保重置了`a`和`b`
a = 1;
b = 2;
var s1 = step( foo );
var s2 = step( bar );
// 首先完全運行`*foo()`
s1();
s1();
s1();
// 現在運行`*bar()`
s2();
s2();
s2();
s2();
console.log( a, b ); // 11 22
```
最終結果是`11`和`22`,就像第一章的版本那樣。現在讓我們把順序混合穿插,來看看它如何改變`a`與`b`的值。
```source-js
// 確保重置了`a`和`b`
a = 1;
b = 2;
var s1 = step( foo );
var s2 = step( bar );
s2(); // b--;
s2(); // 讓出 8
s1(); // a++;
s2(); // a = 8 + b;
// 讓出 2
s1(); // b = b * a;
// 讓出 b
s1(); // a = b + 3;
s2(); // b = a * 2;
```
在我告訴你結果之前,你能指出在前面的程序運行之后`a`和`b`的值是什么嗎?不要作弊!
```source-js
console.log( a, b ); // 12 18
```
注意:?作為留給讀者的練習,試試通過重新安排`s1()`和`s2()`調用的順序,看看你能得到多少種結果組合。別忘了你總是需要三個`s1()`調用和四個`s2()`調用。至于為什么,回想一下剛才關于使用`yield`匹配`next()`的討論。
當然,你幾乎不會想有意制造?*這種*?水平的,令人糊涂的穿插,因為他創建了非常難理解的代碼。但是這個練習很有趣,而且對于理解多個generator如何并發地運行在相同的共享作用域來說很有教育意義,因為會有一些地方這種能力十分有用。
我們會在本章末尾更詳細地討論generator并發。
## 生成值
在前一節中,我們提到了一個generator的有趣用法,作為一種生產值的方式。這?不是?我們本章主要關注的,但如果我們不在這里講一下基本我們會想念它的,特別是因為這種用法實質上是它的名稱的由來:生成器。
我們將要稍稍深入一下?*迭代器*?的話題,但我們會繞回到它們如何與generator關聯,并使用generator來?*生成*?值。
### 發生器與迭代器
想象你正在生產一系列的值,它們中的每一個都與前一個值有可定義的關系。為此,你將需要一個有狀態的發生器來記住上一個給出的值。
你可以用函數閉包(參加本系列的?*作用域與閉包*)來直接地實現這樣的東西:
```source-js
var gimmeSomething = (function(){
var nextVal;
return function(){
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
return nextVal;
};
})();
gimmeSomething(); // 1
gimmeSomething(); // 9
gimmeSomething(); // 33
gimmeSomething(); // 105
```
注意:?這里`nextVal`的計算邏輯已經被簡化了,但從概念上講,直到?*下一次*?`gimmeSomething()`調用發生之前,我們不想計算?*下一個值*(也就是`nextVal`),因為一般對于持久性更強的,或者比簡單的`number`更有限的資源的發生器來說,那可能是一種資源泄漏的設計。
生成隨意的數字序列不是是一個很真實的例子。但是如果你從一個數據源中生成記錄呢?你可以想象很多相同的代碼。
事實上,這種任務是一種非常常見的設計模式,通常用迭代器解決。一個?*迭代器*?是一個明確定義的接口,用來逐個通過一系列從發生器得到的值。迭代器的JS接口,和大多數語言一樣,是在你每次想從發生器中得到下一個值時調用的`next()`。
我們可以為我們的數字序列發生器實現標準的?*迭代器*;
```source-js
var something = (function(){
var nextVal;
return {
// `for..of`循環需要這個
[Symbol.iterator]: function(){ return this; },
// 標準的迭代器接口方法
next: function(){
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
return { done:false, value:nextVal };
}
};
})();
something.next().value; // 1
something.next().value; // 9
something.next().value; // 33
something.next().value; // 105
```
注意:?我們將在“Iterables”一節中講解為什么我們在這個代碼段中需要`[Symbol.iterator]: ..`這一部分。在語法上講,兩個ES6特性在發揮作用。首先,`[ .. ]`語法稱為一個?*計算型屬性名*(參見本系列的?*this與對象原型*)。它是一種字面對象定義方法,用來指定一個表達式并使用這個表達式的結果作為屬性名。另一個,`Symbol.iterator`是ES6預定義的特殊`Symbol`值。
`next()`調用返回一個對象,它帶有兩個屬性:`done`是一個`boolean`值表示?*迭代器*?的完成狀態;`value`持有迭代的值。
ES6還增加了`for..of`循環,它意味著一個標準的?*迭代器*?可以使用原生的循環語法來自動地被消費:
```source-js
for (var v of something) {
console.log( v );
// 不要讓循環永無休止!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
```
注意:?因為我們的`something`迭代器總是返回`done:false`,這個`for..of`循環將會永遠運行,這就是為什么我們條件性地放進一個`break`。對于迭代器來說永不終結是完全沒有問題的,但是也有一些情況?*迭代器*?將運行在有限的值的集合上,而最終返回`done:true`。
`for..of`循環為每一次迭代自動調用`next()`——他不會給`next()`傳入任何值——而且他將會在收到一個`done:true`時自動終結。這對于在一個集合的數據中進行循環十分方便。
當然,你可以手動循環一個迭代器,調用`next()`并檢查`done:true`條件來知道什么時候停止:
```source-js
for (
var ret;
(ret = something.next()) && !ret.done;
) {
console.log( ret.value );
// 不要讓循環永無休止!
if (ret.value > 500) {
break;
}
}
// 1 9 33 105 321 969
```
注意:?這種手動的`for`方式當然要比ES6的`for..of`循環語法難看,但它的好處是它提供給你一個機會,在有必要時傳值給`next(..)`調用。
除了制造你自己的?*迭代器*?之外,許多JS中(就ES6來說)內建的數據結構,比如`array`,也有默認的?*迭代器*:
```source-js
var a = [1,3,5,7,9];
for (var v of a) {
console.log( v );
}
// 1 3 5 7 9
```
`for..of`循環向`a`要來它的迭代器,并自動使用它迭代`a`的值。
注意:?看起來像是一個ES6的奇怪省略,普通的`object`有意地不帶有像`array`那樣的默認?*迭代器*。原因比我們要在這里講的深刻得多。如果你想要的只是迭代一個對象的屬性(不特別保證順序),`Object.keys(..)`返回一個`array`,它可以像`for (var k of Object.keys(obj)) { ..`這樣使用。像這樣用`for..of`循環一個對象上的鍵,與用`for..in`循環內很相似,除了在`for..in`中會包含`[[Prototype]]`鏈的屬性,而`Object.keys(..)`不會(參見本系列的?*this與對象原型*)。
### Iterables
在我們運行的例子中的`something`對象被稱為一個?*迭代器*,因為它的接口中有`next()`方法。但一個緊密關聯的術語是?*iterable*,它指?包含有?一個可以迭代它所有值的迭代器的對象。
在ES6中,從一個?*iterable*?中取得一個?*迭代器*?的方法是,*iterable*?上必須有一個函數,它的名稱是特殊的ES6符號值`Symbol.iterator`。當這個函數被調用時,它就會返回一個?*迭代器*。雖然不是必須的,但一般來說每次調用應當返回一個全新的?*迭代器*。
前一個代碼段的`a`就是一個?*iterable*。`for..of`循環自動地調用它的`Symbol.iterator`函數來構建一個?*迭代器*。我們當然可以手動地調用這個函數,然后使用它返回的?*iterator*:
```source-js
var a = [1,3,5,7,9];
var it = a[Symbol.iterator]();
it.next().value; // 1
it.next().value; // 3
it.next().value; // 5
..
```
在前面定義`something`的代碼段中,你可能已經注意到了這一行:
```source-js
[Symbol.iterator]: function(){ return this; }
```
這段有點讓人困惑的代碼制造了`something`值——`something`*迭代器*?的接口——也是一個?*iterable*;現在它既是一個?*iterable*?也是一個?*迭代器*。然后,我們把`something`傳遞給`for..of`循環:
```source-js
for (var v of something) {
..
}
```
`for..of`循環期待`something`是一個?*iterable*,所以它會尋找并調用它的`Symbol.iterator`函數。我們將這個函數定義為簡單地`return this`,所以它將自己給出,而`for..of`不會知道這些。
### Generator迭代器
帶著?*迭代器*?的背景知識,讓我們把注意力移回generator。一個generator可以被看做一個值的發生器,我們通過一個?*迭代器*接口的`next()`調用每次從中抽取一個值。
所以,一個generator本身在技術上講并不是一個?*iterable*,雖然很相似——當你執行generator時,你就得到一個?*迭代器*:
```source-js
function *foo(){ .. }
var it = foo();
```
我們可以用generator實現早前的`something`無限數字序列發生器,就像這樣:
```source-js
function *something() {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
```
注意:?在一個真實的JS程序中含有一個`while..true`循環通常是一件非常不好的事情,至少如果它沒有一個`break`或`return`語句,那么它就很可能永遠運行,并同步地,阻塞/鎖定瀏覽器UI。然而,在generator中,如果這樣的循環含有一個`yield`,那它就是完全沒有問題的,因為generator將在每次迭代后暫停,`yield`回主程序和/或事件輪詢隊列。說的明白點兒,“generator把`while..true`帶回到JS編程中了!”
這變得相當干凈和簡單點兒了,對吧?因為generator會暫停在每個`yield`,`*something()`函數的狀態(作用域)被保持著,這意味著沒有必要用閉包的模板代碼來跨調用保留變量的狀態了。
不僅是更簡單的代碼——我們不必自己制造?*迭代器*?接口了——它實際上是更合理的代碼,因為它更清晰地表達了意圖。比如,`while..true`循環告訴我們這個generator將要永遠運行——只要我們一直向它請求,它就一直?*產生*?值。
現在我們可以在`for..of`循環中使用新得發亮的`*something()`generator了,而且你會看到它工作起來基本一模一樣:
```source-js
for (var v of something()) {
console.log( v );
// 不要讓循環永無休止!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
```
不要跳過`for (var v of something()) ..`!我們不僅僅像之前的例子那樣將`something`作為一個值引用了,而是調用`*something()`generator來得到它的?*迭代器*,并交給`for..of`使用。
如果你仔細觀察,在這個generator和循環的互動中,你可能會有兩個疑問:
* 為什么我們不能說`for (var v of something) ..`?因為這個`something`是一個generator,而不是一個?*iterable*。我們不得不調用`something()`來構建一個發生器給`for..of`,以便它可以迭代。
* `something()`調用創建一個?*迭代器*,但是`for..of`想要一個?*iterable*,對吧?對,generator的?*迭代器*?上也有一個`Symbol.iterator`函數,這個函數基本上就是`return this`,就像我們剛才定義的`something`*iterable*。換句話說generator的?*迭代器*?也是一個?*iterable*!
#### 停止Generator
在前一個例子中,看起來在循環的`break`被調用后,`*something()`generator的?*迭代器*?實例基本上被留在了一個永遠掛起的狀態。
但是這里有一個隱藏的行為為你處理這件事。`for..of`循環的“異常完成”(“提前終結”等等)——一般是由`break`,`return`,或未捕捉的異常導致的——會向generator的?*迭代器*?發送一個信號,以使它終結。
注意:?技術上講,`for..of`循環也會在循環正常完成時向?*迭代器*?發送這個信號。對于generator來說,這實質上是一個無實際意義的操作,因為generator的?*迭代器*?要首先完成,`for..of`循環才能完成。然而,自定義的?*迭代器*?可能會希望從`for..of`循環的消費者那里得到另外的信號。
雖然一個`for..of`循環將會自動發送這種信號,你可能會希望手動發送信號給一個?*迭代器*;你可以通過調用`return(..)`來這么做。
如果你在generator內部指定一個`try..finally`從句,它將總是被執行,即便是generator從外部被完成。這在你需要進行資源清理時很有用(數據庫連接等):
```source-js
function *something() {
try {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
// 清理用的從句
finally {
console.log( "cleaning up!" );
}
}
```
前面那個在`for..of`中帶有`break`的例子將會觸發`finally`從句。但是你可以用`return(..)`從外部來手動終結generator的?*迭代器*?實例:
```source-js
var it = something();
for (var v of it) {
console.log( v );
// 不要讓循環永無休止!
if (v > 500) {
console.log(
// 使generator得迭代器完成
it.return( "Hello World" ).value
);
// 這里不需要`break`
}
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World
```
當我們調用`it.return(..)`時,它會立即終結generator,從而運行`finally`從句。而且,它會將返回的`value`設置為你傳入`return(..)`的任何東西,這就是`Hellow World`如何立即返回來的。我們現在也不必再包含一個`break`,因為generator的?*迭代器*?會被設置為`done:true`,所以`for..of`循環會在下一次迭代時終結。
generator的命名大部分源自于這種?*消費生產的值*?的用法。但要重申的是,這只是generator的用法之一,而且坦白的說,在這本書的背景下這甚至不是我們主要關注的。
但是現在我們更加全面地了解它們的機制是如何工作的,我們接下來可以將注意力轉向generator如何實施于異步并發。
## 異步地迭代Generator
generator要怎樣處理異步編碼模式,解決回調和類似的問題?讓我們開始回答這個重要的問題。
我們應當重溫一下第三章的一個場景。回想一下這個回調方式:
```source-js
function foo(x,y,cb) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
cb
);
}
foo( 11, 31, function(err,text) {
if (err) {
console.error( err );
}
else {
console.log( text );
}
} );
```
如果我們想用generator表示相同的任務流控制,我們可以:
```source-js
function foo(x,y) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
function(err,data){
if (err) {
// 向`*main()`中扔進一個錯誤
it.throw( err );
}
else {
// 使用收到的`data`來繼續`*main()`
it.next( data );
}
}
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
var it = main();
// 使一切開始運行!
it.next();
```
一眼看上去,這個代碼段要比以前的回調代碼更長,而且也許看起來更復雜。但不要讓這種印象誤導你。generator的代碼段實際上要好?太多?了!但是這里有很多我們需要講解的。
首先,讓我們看看代碼的這一部分,也是最重要的部分:
```source-js
var text = yield foo( 11, 31 );
console.log( text );
```
花一點時間考慮一下這段代碼如何工作。我們調用了一個普通的函數`foo(..)`,而且我們顯然可以從Ajax調用那里得到`text`,即便它是異步的。
這怎么可能?如果你回憶一下第一章的最開始,我們有一個幾乎完全一樣的代碼:
```source-js
var data = ajax( "..url 1.." );
console.log( data );
```
但是這段代碼不好用!你能發現不同嗎?它就是在generator中使用的`yield`。
這就是魔法發生的地方!是它允許我們擁有一個看起來是阻塞的,同步的,但實際上不會阻塞整個程序的代碼;它僅僅暫停/阻塞在generator本身的代碼。
在`yield foo(11,31)`中,首先`foo(11,31)`調用被發起,它什么也不返回(也就是`undefined`),所以我們發起了數據請求,然后我們實際上做的是`yield undefined`。這沒問題,因為這段代碼現在沒有依賴`yield`的值來做任何有趣的事。我們在本章稍后再重新討論這個問題。
在這里,我們沒有將`yield`作為消息傳遞的工具,只是作為進行暫停/阻塞的流程控制的工具。實際上,它會傳遞消息,但是只是單向的,在generator被繼續運行之后。
那么,generator暫停在了`yield`,它實質上再問一個問題,“我該將什么值返回并賦給變量`text`?”誰來回答這個問題?
看一下`foo(..)`。如果Ajax請求成功,我們調用:
```source-js
it.next( data );
```
這將使generator使用應答數據繼續運行,這意味著我們暫停的`yield`表達式直接收到這個值,然后因為它重新開始以運行generator代碼,所以這個值被賦給本地變量`text`。
很酷吧?
退一步考慮一下它的意義。我們在generator內部的代碼看起來完全是同步的(除了`yield`關鍵字本身),但隱藏在幕后的是,在`foo(..)`內部,操作可以完全是異步的。
這很偉大!?這幾乎完美地解決了我們前面遇到的問題:回調不能像我們的大腦可以關聯的那樣,以一種順序,同步的風格表達異步處理。
實質上,我們將異步處理作為實現細節抽象出去,以至于我們可以同步地/順序地推理我們的流程控制:“發起Ajax請求,然后在它完成之后打印應答。” 當然,我們僅僅在這個流程控制中表達了兩個步驟,但同樣的能力可以無邊界地延伸,讓我們需要表達多少步驟,就表達多少。
提示:?這是一個如此重要的認識,為了充分理解,現在回過頭去再把最后三段讀一遍!
### 同步錯誤處理
但是前面的generator代碼會?*讓*?出更多的好處給我們。讓我們把注意力移到generator內部的`try..catch`上:
```source-js
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
```
這是怎么工作的?`foo(..)`調用是異步完成的,`try..catch`不是無法捕捉異步錯誤嗎?就像我們在第三章中看到的?
我們已經看到了`yield`如何讓賦值語句暫停,來等待`foo(..)`去完成,以至于完成的響應可以被賦予`text`。牛X的是,`yield`暫停?*還*?允許generator來`catch`一個錯誤。我們在前面的例子,我們用這一部分代碼將這個錯誤拋出到generator中:
```source-js
if (err) {
// 向`*main()`中扔進一個錯誤
it.throw( err );
}
```
generator的`yield`暫停特性不僅意味著我們可以從異步的函數調用那里得到看起來同步的`return`值,還意味著我們可以同步地捕獲這些異步函數調用的錯誤!
那么我們看到了,我們可以將錯誤?*拋入*?generator,但是將錯誤?*拋出*?一個generator呢?和你期望的一樣:
```source-js
function *main() {
var x = yield "Hello World";
yield x.toLowerCase(); // 引發一個異常!
}
var it = main();
it.next().value; // Hello World
try {
it.next( 42 );
}
catch (err) {
console.error( err ); // TypeError
}
```
當然,我們本可以用`throw ..`手動地拋出一個錯誤,而不是制造一個異常。
我們甚至可以`catch`我們`throw(..)`進generator的同一個錯誤,實質上給了generator一個機會來處理它,但如果generator沒處理,那么?*迭代器*?代碼必須處理它:
```source-js
function *main() {
var x = yield "Hello World";
// 永遠不會跑到這里
console.log( x );
}
var it = main();
it.next();
try {
// `*main()`會處理這個錯誤嗎?我們走著瞧!
it.throw( "Oops" );
}
catch (err) {
// 不,它沒處理!
console.error( err ); // Oops
}
```
使用異步代碼的,看似同步的錯誤處理(通過`try..catch`)在可讀性和可推理性上大獲全勝。
## Generators + Promises
在我們前面的討論中,我們展示了generator如何可以異步地迭代,這是一個用順序的可推理性來取代混亂如面條的回調的一個巨大進步。但我們丟掉了兩個非常重要的東西:Promise的可靠性和可組合性(見第三章)!
別擔心——我們會把它們拿回來。在ES6的世界中最棒的就是將generator(看似同步的異步代碼)與Promise(可靠性和可組合性)組合起來。
但怎么做呢?
回想一下第三章中我們基于Promise的方式運行Ajax的例子:
```source-js
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
foo( 11, 31 )
.then(
function(text){
console.log( text );
},
function(err){
console.error( err );
}
);
```
在我們早先的運行Ajax的例子的generator代碼中,`foo(..)`什么也不返回(`undefined`),而且我們的?*迭代器*?控制代碼也不關心`yield`的值。
但這里的Promise相關的`foo(..)`在發起Ajax調用后返回一個promise。這暗示著我們可以用`foo(..)`構建一個promise,然后從generator中`yield`出來,而后?*迭代器*?控制代碼將可以收到這個promise。
那么?*迭代器*?應當對promise做什么?
它應當監聽promise的解析(完成或拒絕),然后要么使用完成消息繼續運行generator,要么使用拒絕理由向generator拋出錯誤。
讓我重復一遍,因為它如此重要。發揮Promise和generator的最大功效的自然方法是?`yield`一個Promise,并將這個Promise連接到generator的?*迭代器*?的控制端。
讓我們試一下!首先,我們將Promise相關的`foo(..)`與generator`*main()`放在一起:
```source-js
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
```
在這個重構中最強大的啟示是,`*main()`內部的代碼?更本就沒變!?在generator內部,無論什么樣的值被`yield`出去都是一個不可見的實現細節,所以我們甚至不會察覺它發生了,也不用擔心它。
那么我們現在如何運行`*main()`?我們還有一些管道的實現工作要做,接收并連接`yield`的promise,使它能夠根據解析來繼續運行generator。我們從手動這么做開始:
```source-js
var it = main();
var p = it.next().value;
// 等待`p` promise解析
p.then(
function(text){
it.next( text );
},
function(err){
it.throw( err );
}
);
```
其實,根本不費事,對吧?
這段代碼應當看起來與我們早前做的很相似:手動地連接被錯誤優先的回調控制的generator。與`if (err) { it.throw..`不同的是,promise已經為我們分割為完成(成功)與拒絕(失敗),否則?*迭代器*?控制是完全相同的。
現在,我們已經掩蓋了一些重要的細節。
最重要的是,我們利用了這樣一個事實:我們知道`*main()`里面只有一個Promise相關的步驟。如果我們想要能用Promise驅動一個generator而不管它有多少步驟呢?我們當然不想為每一個generator手動編寫一個不同的Promise鏈!要是有這樣一種方法該多好:可以重復(也就是“循環”)迭代的控制,而且每次一有Promise出來,就在繼續之前等待它的解析。
另外,如果generator在`it.next()`調用期間拋出一個錯誤怎么辦?我們是該退出,還是應該`catch`它并把它送回去?相似地,要是我們`it.throw(..)`一個Promise拒絕給generator,但是沒有被處理,又直接回來了呢?
### 帶有Promise的Generator運行器
你在這條路上探索得越遠,你就越能感到,“哇,要是有一些工具能幫我做這些就好了。”而且你絕對是對的。這是一種如此重要的模式,而且你不想把它弄錯(或者因為一遍又一遍地重復它而把自己累死),所以你最好的選擇是把賭注壓在一個工具上,而它以我們將要描述的方式使用這種特定設計的工具來?*運行*?`yield`Promise的generator。
有幾種Promise抽象庫提供了這樣的工具,包括我的?*asynquence*?庫和它的`runner(..)`,我們將在本書的在附錄A中討論它。
但看在學習和講解的份兒上,讓我們定義我們自己的名為`run(..)`的獨立工具:
```source-js
// 感謝Benjamin Gruenbaum (@benjamingr在GitHub)在此做出的巨大改進!
function run(gen) {
var args = [].slice.call( arguments, 1), it;
// 在當前的上下文環境中初始化generator
it = gen.apply( this, args );
// 為generator的完成返回一個promise
return Promise.resolve()
.then( function handleNext(value){
// 運行至下一個讓出的值
var next = it.next( value );
return (function handleResult(next){
// generator已經完成運行了?
if (next.done) {
return next.value;
}
// 否則繼續執行
else {
return Promise.resolve( next.value )
.then(
// 在成功的情況下繼續異步循環,將解析的值送回generator
handleNext,
// 如果`value`是一個拒絕的promise,就將錯誤傳播回generator自己的錯誤處理g
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
})(next);
} );
}
```
如你所見,它可能比你想要自己編寫的東西復雜得多,特別是你將不會想為每個你使用的generator重復這段代碼。所以,一個幫助工具/庫絕對是可行的。雖然,我鼓勵你花幾分鐘時間研究一下這點代碼,以便對如何管理generator+Promise交涉得到更好的感覺。
你如何在我們?*正在討論*?的Ajax例子中將`run(..)`和`*main()`一起使用呢?
```source-js
function *main() {
// ..
}
run( main );
```
就是這樣!按照我們連接`run(..)`的方式,它將自動地,異步地推進你傳入的generator,直到完成。
注意:?我們定義的`run(..)`返回一個promise,它被連接成一旦generator完成就立即解析,或者收到一個未捕獲的異常,而generator沒有處理它。我們沒有在這里展示這種能力,但我們會在本章稍后回到這個話題。
#### ES7:?`async`?和?`await`?
前面的模式——generator讓出一個Promise,然后這個Promise控制generator的?*迭代器*?向前推進至它完成——是一個如此強大和有用的方法,如果我們能不通過亂七八糟的幫助工具庫(也就是`run(..)`)來使用它就更好了。
在這方面可能有一些好消息。在寫作這本書的時候,后ES6,ES7化的時間表上已經出現了草案,對這個問題提供早期但強大的附加語法支持。顯然,現在還太早而不能保證其細節,但是有相當大的可能性它將蛻變為類似于下面的東西:
```source-js
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
async function main() {
try {
var text = await foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
main();
```
如你所見,這里沒有`run(..)`調用(意味著不需要工具庫!)來驅動和調用`main()`——它僅僅像一個普通函數那樣被調用。另外,`main()`不再作為一個generator函數聲明;它是一種新型的函數:`async function`。而最后,與`yield`一個Promise相反,我們`await`它解析。
如果你`await`一個Promise,`async function`會自動地知道做什么——它會暫停這個函數(就像使用generator那樣)直到Promise解析。我們沒有在這個代碼段中展示,但是調用一個像`main()`這樣的異步函數將自動地返回一個promise,它會在函數完全完成時被解析。
提示:?`async`?/?`await`的語法應該對擁有C#經驗的讀者看起來非常熟悉,因為它們基本上是一樣的。
這個草案實質上是為我們已經衍生出的模式進行代碼化的支持,成為一種語法機制:用看似同步的流程控制代碼與Promise組合。將兩個世界的最好部分組合,來有效解決我們用回調遇到的幾乎所有主要問題。
這樣的ES7化草案已經存在,并且有了早期的支持和熱忱的擁護。這一事實為這種異步模式在未來的重要性上信心滿滿地投了有力的一票。
### Generator中的Promise并發
至此,所有我們展示過的是一種使用Promise+generator的單步異步流程。但是現實世界的代碼將總是有許多異步步驟。
如果你不小心,generator看似同步的風格也許會蒙蔽你,使你在如何構造你的異步并發上感到自滿,導致性能次優的模式。那么我們想花一點時間來探索一下其他選項。
想象一個場景,你需要從兩個不同的數據源取得數據,然后將這些應答組合來發起第三個請求,最后打印出最終的應答。我們在第三章中用Promise探索過類似的場景,但這次讓我們在generator的環境下考慮它。
你的第一直覺可能是像這樣的東西:
```source-js
function *foo() {
var r1 = yield request( "http://some.url.1" );
var r2 = yield request( "http://some.url.2" );
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// 使用剛才定義的`run(..)`工具
run( foo );
```
這段代碼可以工作,但在我們特定的這個場景中,它不是最優的。你能發現為什么嗎?
因為`r1`和`r2`請求可以——而且為了性能的原因,*應該*——并發運行,但在這段代碼中它們將順序地運行;直到`"http://some.url.1"`請求完成之前,`"http://some.url.2"`URL不會被Ajax取得。這兩個請求是獨立的,所以性能更好的方式可能是讓它們同時運行。
但是使用generator和`yield`,到底應該怎么做?我們知道`yield`在代碼中只是一個單獨的暫停點,所以你根本不能再同一時刻做兩次暫停。
最自然和有效的答案是基于Promise的異步流程,特別是因為它們的時間無關的狀態管理能力(參見第三章的“未來的值”)。
最簡單的方式:
```source-js
function *foo() {
// 使兩個請求“并行”
var p1 = request( "http://some.url.1" );
var p2 = request( "http://some.url.2" );
// 等待兩個promise都被解析
var r1 = yield p1;
var r2 = yield p2;
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// 使用剛才定義的`run(..)`工具
run( foo );
```
為什么這與前一個代碼段不同?看看`yield`在哪里和不在哪里。`p1`和`p2`是并發地(也就是“并行”)發起的Ajax請求promise。它們哪一個先完成都不要緊,因為promise會一直保持它們的解析狀態。
然后我們使用兩個連續的`yield`語句等待并從promise中取得解析值(分別取到`r1`和`r2`中)。如果`p1`首先解析,`yield p1`會首先繼續執行然后等待`yield p2`繼續執行。如果`p2`首先解析,它將會耐心地保持解析值知道被請求,但是`yield p1`將會首先停住,直到`p1`解析。
不管是哪一種情況,`p1`和`p2`都將并發地運行,并且在`r3 = yield request..`Ajax請求發起之前,都必須完成,無論以哪種順序。
如果這種流程控制處理模型聽起來很熟悉,那是因為它基本上和我們在第三章中介紹的,因`Promise.all([ .. ])`工具成為可能的“門”模式是相同的。所以,我們也可以像這樣表達這種流程控制:
```source-js
function *foo() {
// 使兩個請求“并行”并等待兩個promise都被解析
var results = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" )
] );
var r1 = results[0];
var r2 = results[1];
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// 使用前面定義的`run(..)`工具
run( foo );
```
注意:?就像我們在第三章中討論的,我們甚至可以用ES6解構賦值來把`var r1 = .. var r2 = ..`賦值簡寫為`var [r1,r2] = results`。
換句話說,在generator+Promise的方式中,Promise所有的并發能力都是可用的。所以在任何地方,如果你需要比“這個然后那個”要復雜的順序異步流程步驟時,Promise都可能是最佳選擇。
#### Promises,隱藏起來
作為代碼風格的警告要說一句,要小心你在?你的generator內部?包含了多少Promise邏輯。以我們描述過的方式在異步性上使用generator的全部意義,是要創建簡單,順序,看似同步的代碼,并盡可能多地將異步性細節隱藏在這些代碼之外。
比如,這可能是一種更干凈的方式:
```source-js
// 注意:這是一個普通函數,不是generator
function bar(url1,url2) {
return Promise.all( [
request( url1 ),
request( url2 )
] );
}
function *foo() {
// 將基于Promise的并發細節隱藏在`bar(..)`內部
var results = yield bar(
"http://some.url.1",
"http://some.url.2"
);
var r1 = results[0];
var r2 = results[1];
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// 使用剛才定義的`run(..)`工具
run( foo );
```
在`*foo()`內部,它更干凈更清晰地表達了我們要做的事情:我們要求`bar(..)`給我們一些`results`,而我們將用`yield`等待它的發生。我們不必關心在底層一個`Promise.all([ .. ])`的Promise組合將被用來完成任務。
我們將異步性,特別是Promise,作為一種實現細節。
如果你要做一種精巧的序列流控制,那么將你的Promise邏輯隱藏在一個僅僅從你的generator中調用的函數里特別有用。舉個例子:
```source-js
function bar() {
return Promise.all( [
baz( .. )
.then( .. ),
Promise.race( [ .. ] )
] )
.then( .. )
}
```
有時候這種邏輯是必須的,而如果你直接把它扔在你的generator內部,你就違背了大多數你使用generator的初衷。我們?*應當*有意地將這樣的細節從generator代碼中抽象出去,以使它們不會搞亂更高層的任務表達。
在創建功能強與性能好的代碼之上,你還應當努力使代碼盡可能地容易推理和維護。
注意:?對于編程來說,抽象不總是一種健康的東西——許多時候它可能在得到簡潔的同時增加復雜性。但是在這種情況下,我相信你的generator+Promise異步代碼要比其他的選擇健康得多。雖然有所有這些建議,你仍然要注意你的特殊情況,并為你和你的團隊做出合適的決策。
## Generator 委托
在上一節中,我們展示了從generator內部調用普通函數,和它如何作為一種有用的技術來將實現細節(比如異步Promise流程)抽象出去。但是為這樣的任務使用普通函數的缺陷是,它必須按照普通函數的規則行動,也就是說它不能像generator那樣用`yield`來暫停自己。
在你身上可能發生這樣的事情:你可能會試著使用我們的`run(..)`幫助函數,從一個generator中調用另個一generator。比如:
```source-js
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// 通過`run(..)`“委托”到`*foo()`
var r3 = yield run( foo );
console.log( r3 );
}
run( bar );
```
通過再一次使用我們的`run(..)`工具,我們在`*bar()`內部運行`*foo()`。我們利用了這樣一個事實:我們早先定義的`run(..)`返回一個promise,這個promise在generator運行至完成時才解析(或發生錯誤),所以如果我們從一個`run(..)`調用中`yield`出一個promise給另一個`run(..)`,它就會自動暫停`*bar()`直到`*foo()`完成。
但這里有一個更好的辦法將`*foo()`調用整合進`*bar()`,它稱為`yield`委托。`yield`委托的特殊語法是:`yield * __`(注意額外的`*`)。讓它在我們前面的例子中工作之前,讓我們看一個更簡單的場景:
```source-js
function *foo() {
console.log( "`*foo()` starting" );
yield 3;
yield 4;
console.log( "`*foo()` finished" );
}
function *bar() {
yield 1;
yield 2;
yield *foo(); // `yield`-delegation!
yield 5;
}
var it = bar();
it.next().value; // 1
it.next().value; // 2
it.next().value; // `*foo()` starting
// 3
it.next().value; // 4
it.next().value; // `*foo()` finished
// 5
```
注意:?在本章早前的一個注意點中,我解釋了為什么我偏好`function *foo() ..`而不是`function* foo() ..`,相似地,我也偏好——與關于這個話題的其他大多數文檔不同——說`yield *foo()`而不是`yield* foo()`。`*`的擺放是純粹的風格問題,而且要看你的最佳判斷。但我發現保持統一風格很吸引人。
`yield *foo()`委托是如何工作的?
首先,正如我們看到過的那樣,調用`foo()`創建了一個?*迭代器*。然后,`yield *`將(當前`*bar()`generator的)?*迭代器*?的控制委托/傳遞給這另一個`*foo()`*迭代器*。
那么,前兩個`it.next()`調用控制著`*bar()`,但當我們發起第三個`it.next()`調用時,`*foo()`就啟動了,而且這時我們控制的是`*foo()`而非`*bar()`。這就是為什么它稱為委托——`*bar()`將它的迭代控制委托給`*foo()`。
只要`it`*迭代器*?的控制耗盡了整個`*foo()`*迭代器*,它就會自動地將控制返回到`*bar()`。
那么現在回到前面的三個順序Ajax請求的例子:
```source-js
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// 通過`run(..)`“委托”到`*foo()`
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
```
這個代碼段和前面使用的版本的唯一區別是,使用了`yield *foo()`而不是前面的`yield run(foo)`。
注意:?`yield *`讓出了迭代控制,不是generator控制;當你調用`*foo()`generator時,你就`yield`委托給它的?*迭代器*。但你實際上可以`yield`委托給任何?*迭代器*;`yield *[1,2,3]`將會消費默認的`[1,2,3]`數組值?*迭代器*。
### 為什么委托?
`yield`委托的目的很大程度上是為了代碼組織,而且這種方式是與普通函數調用對稱的。
想象兩個分別提供了`foo()`和`bar()`方法的模塊,其中`bar()`調用`foo()`。它們倆分開的原因一般是由于為了程序將它們作為分離的程序來調用而進行的恰當組織。例如,可能會有一些情況`foo()`需要被獨立調用,而其他地方`bar()`來調用`foo()`。
由于這些完全相同的原因,將generator分開可以增強程序的可讀性,可維護性,與可調試性。從這個角度講,`yield *`是一種快捷的語法,用來在`*bar()`內部手動地迭代`*foo()`的步驟。
如果`*foo()`中的步驟是異步的,這樣的手動方式可能會特別復雜,這就是為什么你可能會需要那個`run(..)`工具來做它。正如我們已經展示的,`yield *foo()`消滅了使用`run(..)`工具的子實例(比如`run(foo)`)的需要。
### 委托消息
你可能想知道,這種`yield`委托在除了與?*迭代器*?控制一起工作以外,是如何與雙向消息傳遞一起工作的。仔細查看下面這些通過`yield`委托進進出出的消息流:
```source-js
function *foo() {
console.log( "inside `*foo()`:", yield "B" );
console.log( "inside `*foo()`:", yield "C" );
return "D";
}
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-委托!
console.log( "inside `*bar()`:", yield *foo() );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// inside `*foo()`: 3
// inside `*bar()`: D
// outside: E
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: 4
// outside: F
```
特別注意一下`it.next(3)`調用之后的處理步驟:
1. 值`3`被傳入(通過`*bar`里的`yield`委托)在`*foo()`內部等待中的`yield "C"`表達式。
2. 然后`*foo()`調用`return "D"`,但是這個值不會一路返回到外面的`it.next(3)`調用。
3. 相反地,值`"D"`作為結果被發送到在`*bar()`內部等待中的`yield *foo()`表示式——這個`yield`委托表達式實質上在`*foo()`被耗盡之前一直被暫停著。所以`"D"`被送到`*bar()`內部來讓它打印。
4. `yield "E"`在`*bar()`內部被調用,而且值`"E"`被讓出到外部作為`it.next(3)`調用的結果。
從外部?*迭代器*(`it`)的角度來看,在初始的generator和被委托的generator之間的控制沒有任何區別。
事實上,`yield`委托甚至不必指向另一個generator;它可以僅被指向一個非generator的,一般的?*iterable*。比如:
```source-js
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-委托至一個非generator
console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// outside: C
console.log( "outside:", it.next( 3 ).value );
// outside: D
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: undefined
// outside: E
console.log( "outside:", it.next( 5 ).value );
// inside `*bar()`: 5
// outside: F
```
注意這個例子與前一個之間,被接收/報告的消息的不同之處。
最驚人的是,默認的`array`*迭代器*?不關心任何通過`next(..)`調用被發送的消息,所以值`2`,`3`,與`4`實質上被忽略了。另外,因為這個?*迭代器*?沒有明確的`return`值(不像前面使用的`*foo()`),所以`yield *`表達式在它完成時得到一個`undefined`。
#### 異常也委托!
與`yield`委托在兩個方向上透明地傳遞消息的方式相同,錯誤/異常也在雙向傳遞:
```source-js
function *foo() {
try {
yield "B";
}
catch (err) {
console.log( "error caught inside `*foo()`:", err );
}
yield "C";
throw "D";
}
function *bar() {
yield "A";
try {
yield *foo();
}
catch (err) {
console.log( "error caught inside `*bar()`:", err );
}
yield "E";
yield *baz();
// note: can't get here!
yield "G";
}
function *baz() {
throw "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// outside: B
console.log( "outside:", it.throw( 2 ).value );
// error caught inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// error caught inside `*bar()`: D
// outside: E
try {
console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
console.log( "error caught outside:", err );
}
// error caught outside: F
```
在這段代碼中有一些事情要注意:
1. 但我們調用`it.throw(2)`時,它發送一個錯誤消息`2`到`*bar()`,而`*bar()`將它委托至`*foo()`,然后`*foo()`來`catch`它并平靜地處理。之后,`yield "C"`把`"C"`作為返回的`value`發送回`it.throw(2)`調用。
2. 接下來值`"D"`被從`*foo()`內部`throw`出來并傳播到`*bar()`,`*bar()`會`catch`它并平靜地處理。然后`yield "E"`把`"E"`作為返回的`value`發送回`it.next(3)`調用。
3. 接下來,一個異常從`*baz()`中`throw`出來,而沒有被`*bar()`捕獲——我們沒在外面`catch`它——所以`*baz()`和`*bar()`都被設置為完成狀態。這段代碼結束后,即便有后續的`next(..)`調用,你也不會得到值`"G"`——它們的`value`將返回`undefined`。
### 異步委托
最后讓我們回到早先的多個順序Ajax請求的例子,使用`yield`委托:
```source-js
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
```
在`*bar()`內部,與調用`yield run(foo)`不同的是,我們調用`yield *foo()`就可以了。
在前一個版本的這個例子中,Promise機制(通過`run(..)`控制的)被用于將值從`*foo()`中的`return r3`傳送到`*bar()`內部的本地變量`r3`。現在,這個值通過`yield *`機制直接返回。
除此以外,它們的行為是一樣的。
### “遞歸”委托
當然,`yield`委托可以一直持續委托下去,你想連接多少步驟就連接多少。你甚至可以在具有異步能力的generator上“遞歸”使用`yield`委托——一個`yield`委托至自己的generator:
```source-js
function *foo(val) {
if (val > 1) {
// 遞歸委托
val = yield *foo( val - 1 );
}
return yield request( "http://some.url/?v=" + val );
}
function *bar() {
var r1 = yield *foo( 3 );
console.log( r1 );
}
run( bar );
```
注意:?我們的`run(..)`工具本可以用`run( foo, 3 )`來調用,因為它支持用額外傳遞的參數來進行generator的初始化。然而,為了在這里高調展示`yield *`的靈活性,我們使用了無參數的`*bar()`。
這段代碼之后的處理步驟是什么?堅持住,它的細節要描述起來可是十分錯綜復雜:
1. `run(bar)`啟動了`*bar()`generator。
2. `foo(3)`為`*foo(..)`創建了?*迭代器*?并傳遞`3`作為它的`val`參數。
3. 因為`3 > 1`,`foo(2)`創建了另一個?*迭代器*?并傳遞`2`作為它的`val`參數。
4. 因為`2 > 1`,`foo(1)`又創建了另一個?*迭代器*?并傳遞`1`作為它的`val`參數。
5. `1 > 1`是`false`,所以我們接下來用值`1`調用`request(..)`,并得到一個代表第一個Ajax調用的promise。
6. 這個promise被`yield`出來,回到`*foo(2)`generator實例。
7. `yield *`將這個promise傳出并回到`*foo(3)`生成generator。另一個`yield *`把這個promise傳出到`*bar()`generator實例。而又有另一個`yield *`把這個promise傳出到`run(..)`工具,而它將會等待這個promise(第一個Ajax請求)再處理。
8. 當這個promise解析時,它的完成消息會被發送以繼續`*bar()`,`*bar()`通過`yield *`把消息傳遞進`*foo(3)`實例,`*foo(3)`實例通過`yield *`把消息傳遞進`*foo(2)`generator實例,`*foo(2)`實例通過`yield *`把消息傳給那個在`*foo(3)`generator實例中等待的一般的`yield`。
9. 這第一個Ajax調用的應答現在立即從`*foo(3)`generator實例中被`return`,作為`*foo(2)`實例中`yield *`表達式的結果發送回來,并賦值給本地`val`變量。
10. `*foo(2)`內部,第二個Ajax請求用`request(..)`發起,它的promise被`yield`回到`*foo(1)`實例,然后一路`yield *`傳播到`run(..)`(回到第7步)。當promise解析時,第二個Ajax應答一路傳播回到`*foo(2)`generator實例,并賦值到他本地的`val`變量。
11. 最終,第三個Ajax請求用`request(..)`發起,它的promise走出到`run(..)`,然后它的解析值一路返回,最后被`return`到在`*bar()`中等待的`yield *`表達式。
天!許多瘋狂的頭腦雜技,對吧?你可能想要把它通讀幾遍,然后抓點兒零食放松一下大腦!
## Generator并發
正如我們在第一章和本章早先討論過的,另個同時運行的“進程”可以協作地穿插它們的操作,而且許多時候這可以產生非常強大的異步表達式。
坦白地說,我們前面關于多個generator并發穿插的例子,展示了這真的容易讓人糊涂。但我們也受到了啟發,有些地方這種能力十分有用。
回想我們在第一章中看過的場景,兩個不同但同時的Ajax應答處理需要互相協調,來確保數據通信不是竟合狀態。我們這樣把應答分別放在`res`數組的不同位置中:
```source-js
function response(data) {
if (data.url == "http://some.url.1") {
res[0] = data;
}
else if (data.url == "http://some.url.2") {
res[1] = data;
}
}
```
但是我們如何在這種場景下使用多generator呢?
```source-js
// `request(..)` 是一個基于Promise的Ajax工具
var res = [];
function *reqData(url) {
res.push(
yield request( url )
);
}
```
注意:?我們將在這里使用兩個`*reqData(..)`generator的實例,但是這和分別使用兩個不同generator的一個實例沒有區別;這兩種方式在道理上完全一樣的。我們過一會兒就會看到兩個generator的協調操作。
與不得不將`res[0]`和`res[1]`賦值手動排序不同,我們將使用協調過的順序,讓`res.push(..)`以可預見的順序恰當地將值放在預期的位置。如此被表達的邏輯會讓人感覺更干凈。
但是我們將如何實際安排這種互動呢?首先,讓我們手動實現它:
```source-js
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it1.next().value;
var p2 = it2.next().value;
p1
.then( function(data){
it1.next( data );
return p2;
} )
.then( function(data){
it2.next( data );
} );
```
`*reqData(..)`的兩個實例都開始發起它們的Ajax請求,然后用`yield`暫停。之后我們再`p1`解析時繼續運行第一個實例,而后來的`p2`的解析將會重啟第二個實例。以這種方式,我們使用Promise的安排來確保`res[0]`將持有第一個應答,而`res[1]`持有第二個應答。
但坦白地說,這是可怕的手動,而且它沒有真正讓generator組織它們自己,而那才是真正的力量。讓我們用不同的方法試一下:
```source-js
// `request(..)` 是一個基于Promise的Ajax工具
var res = [];
function *reqData(url) {
var data = yield request( url );
// 傳遞控制權
yield;
res.push( data );
}
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it1.next().value;
var p2 = it2.next().value;
p1.then( function(data){
it1.next( data );
} );
p2.then( function(data){
it2.next( data );
} );
Promise.all( [p1,p2] )
.then( function(){
it1.next();
it2.next();
} );
```
好的,這看起來好些了(雖然仍然是手動),因為現在兩個`*reqData(..)`的實例真正地并發運行了,而且(至少是在第一部分)是獨立的。
在前一個代碼段中,第二個實例在第一個實例完全完成之前沒有給出它的數據。但是這里,只要它們的應答一返回這兩個實例就立即分別收到他們的數據,然后每個實例調用另一個`yield`來傳送控制。最后我們在`Promise.all([ .. ])`的處理器中選擇用什么樣的順序繼續它們。
可能不太明顯的是,這種方式因其對稱性啟發了一種可復用工具的簡單形式。讓我們想象使用一個稱為`runAll(..)`的工具:
```source-js
// `request(..)` 是一個基于Promise的Ajax工具
var res = [];
runAll(
function*(){
var p1 = request( "http://some.url.1" );
// 傳遞控制權
yield;
res.push( yield p1 );
},
function*(){
var p2 = request( "http://some.url.2" );
// 傳遞控制權
yield;
res.push( yield p2 );
}
);
```
注意:?我們沒有包含`runAll(..)`的實現代碼,不僅因為它長得無法行文,也因為它是一個我們已經在先前的?`run(..)`中實現的邏輯的擴展。所以,作為留給讀者的一個很好的補充性練習,請你自己動手改進`run(..)`的代碼,來使它像想象中的`runAll(..)`那樣工作。另外,我的?*asynquence*?庫提供了一個前面提到過的`runner(..)`工具,它內建了這種能力,我們將在本書的附錄A中討論它。
這是`runAll(..)`內部的處理將如何操作:
1. 第一個generator得到一個代表從`"http://some.url.1"`來的Ajax應答,然后將控制權`yield`回到`runAll(..)`工具。
2. 第二個generator運行,并對`"http://some.url.2"`做相同的事,將控制權`yield`回到`runAll(..)`工具。
3. 第一個generator繼續,然后`yield`出他的promise`p1`。在這種情況下`runAll(..)`工具和我們前面的`run(..)`做同樣的事,它等待promise解析,然后繼續這同一個generator(沒有控制傳遞!)。當`p1`解析時,`runAll(..)`使用解析值再一次繼續第一個generator,而后`res[0]`得到它的值。在第一個generator完成之后,有一個隱式的控制權傳遞。
4. 第二個generator繼續,`yield`出它的promise`p2`,并等待它的解析。一旦`p2`解析,`runAll(..)`使用這個解析值繼續第二個generator,于是`res[1]`被設置。
在這個例子中,我們使用了一個稱為`res`的外部變量來保存兩個不同的Ajax應答的結果——這是我們的并發協調。
但是這樣做可能十分有幫助:進一步擴展`runAll(..)`使它為多個generator實例提供?*分享的*?內部的變量作用域,比如一個我們將在下面稱為`data`的空對象。另外,它可以接收被`yield`的非Promise值,并把它們交給下一個generator。
考慮這段代碼:
```source-js
// `request(..)` 是一個基于Promise的Ajax工具
runAll(
function*(data){
data.res = [];
// 傳遞控制權(并傳遞消息)
var url1 = yield "http://some.url.2";
var p1 = request( url1 ); // "http://some.url.1"
// 傳遞控制權
yield;
data.res.push( yield p1 );
},
function*(data){
// 傳遞控制權(并傳遞消息)
var url2 = yield "http://some.url.1";
var p2 = request( url2 ); // "http://some.url.2"
// 傳遞控制權
yield;
data.res.push( yield p2 );
}
);
```
在這個公式中,兩個generator不僅協調控制傳遞,實際上還互相通信:通過`data.res`,和交換`url1`與`url2`的值的`yield`消息。這強大到不可思議!
這樣的認識也是一種更為精巧的稱為CSP(Communicating Sequential Processes——通信順序處理)的異步技術的概念基礎,我們將在本書的附錄B中討論它。
## Thunks
至此,我們都假定從一個generator中`yield`一個Promise——讓這個Promise使用像`run(..)`這樣的幫助工具來推進generator——是管理使用generator的異步處理的最佳方法。明白地說,它是的。
但是我們跳過了一個被輕度廣泛使用的模式,為了完整性我們將簡單地看一看它。
在一般的計算機科學中,有一種老舊的前JS時代的概念,稱為“thunk”。我們不在這里贅述它的歷史,一個狹隘的表達是,thunk是一個JS函數——沒有任何參數——它連接并調用另一個函數。
換句話講,你用一個函數定義包裝函數調用——帶著它需要的所有參數——來?*推遲*?這個調用的執行,而這個包裝用的函數就是thunk。當你稍后執行thunk時,你最終會調用那個原始的函數。
舉個例子:
```source-js
function foo(x,y) {
return x + y;
}
function fooThunk() {
return foo( 3, 4 );
}
// 稍后
console.log( fooThunk() ); // 7
```
所以,一個同步的thunk是十分直白的。但是一個異步的thunk呢?我們實質上可以擴展這個狹隘的thunk定義,讓它接收一個回調。
考慮這段代碼:
```source-js
function foo(x,y,cb) {
setTimeout( function(){
cb( x + y );
}, 1000 );
}
function fooThunk(cb) {
foo( 3, 4, cb );
}
// 稍后
fooThunk( function(sum){
console.log( sum ); // 7
} );
```
如你所見,`fooThunk(..)`僅需要一個`cb(..)`參數,因為它已經預先制定了值`3`和`4`(分別為`x`和`y`)并準備傳遞給`foo(..)`。一個thunk只是在外面耐心地等待著它開始工作所需的最后一部分信息:回調。
但是你不會想要手動制造thunk。那么,讓我們發明一個工具來為我們進行這種包裝。
考慮這段代碼:
```source-js
function thunkify(fn) {
var args = [].slice.call( arguments, 1 );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
}
var fooThunk = thunkify( foo, 3, 4 );
// 稍后
fooThunk( function(sum) {
console.log( sum ); // 7
} );
```
提示:?這里我們假定原始的(`foo(..)`)函數簽名希望它的回調的位置在最后,而其它的參數在這之前。這是一個異步JS函數的相當普遍的“標準”。你可以稱它為“回調后置風格”。如果因為某些原因你需要處理“回調優先風格”的簽名,你只需要制造一個使用`args.unshift(..)`而非`args.push(..)`的工具。
前面的`thunkify(..)`公式接收`foo(..)`函數的引用,和任何它所需的參數,并返回thunk本身(`fooThunk(..)`)。然而,這并不是你將在JS中發現的thunk的典型表達方式。
與`thunkify(..)`制造thunk本身相反,典型的——可能有點兒讓人困惑的——`thunkify(..)`工具將產生一個制造thunk的函數。
額...是的。
考慮這段代碼:
```source-js
function thunkify(fn) {
return function() {
var args = [].slice.call( arguments );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
};
}
```
這里主要的不同之處是有一個額外的`return function() { .. }`。這是它在用法上的不同:
```source-js
var whatIsThis = thunkify( foo );
var fooThunk = whatIsThis( 3, 4 );
// 稍后
fooThunk( function(sum) {
console.log( sum ); // 7
} );
```
明顯地,這段代碼隱含的最大的問題是,`whatIsThis`叫什么合適?它不是thunk,它是一個從`foo(..)`調用生產thunk的東西。它是一種“thunk”的“工廠”。而且看起來沒有任何標準的意見來命名這種東西。
所以,我的提議是“thunkory”("thunk" + "factory")。于是,`thunkify(..)`制造了一個thunkory,而一個thunkory制造thunks。這個道理與第三章中我的“promisory”提議是對稱的:
```source-js
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// 稍后
fooThunk1( function(sum) {
console.log( sum ); // 7
} );
fooThunk2( function(sum) {
console.log( sum ); // 11
} );
```
注意:?這個例子中的`foo(..)`期望的回調不是“錯誤優先風格”。當然,“錯誤優先風格”更常見。如果`foo(..)`有某種合理的錯誤發生機制,我們可以改變而使它期望并使用一個錯誤優先的回調。后續的`thunkify(..)`不會關心回調被預想成什么樣。用法的唯一區別是`fooThunk1(function(err,sum){..`。
暴露出thunkory方法——而不是像早先的`thunkify(..)`那樣將中間步驟隱藏起來——可能看起來像是沒必要的混亂。但是一般來講,在你的程序一開始就制造一些thunkory來包裝既存API的方法是十分有用的,然后你就可以在你需要thunk的時候傳遞并調用這些thunkory。這兩個區別開的步驟保證了功能上更干凈的分離。
來展示一下的話:
```source-js
// 更干凈:
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// 而這個不干凈:
var fooThunk1 = thunkify( foo, 3, 4 );
var fooThunk2 = thunkify( foo, 5, 6 );
```
不管你是否愿意明確對付thunkory,thunk(`fooThunk1(..)`和`fooThunk2(..)`)的用法還是一樣的。
### s/promise/thunk/
那么所有這些thunk的東西與generator有什么關系?
一般性地比較一下thunk和promise:它們是不能直接互換的,因為它們在行為上不是等價的。比起單純的thunk,Promise可用性更廣泛,而且更可靠。
但從另一種意義上講,它們都可以被看作是對一個值的請求,這個請求可能被異步地應答。
回憶第三章,我們定義了一個工具來promise化一個函數,我們稱之為`Promise.wrap(..)`——我們本來也可以叫它`promisify(..)`的!這個Promise化包裝工具不會生產Promise;它生產那些繼而生產Promise的promisories。這和我們當前討論的thunkory和thunk是完全對稱的。
為了描繪這種對稱性,讓我們首先將`foo(..)`的例子改為假定一個“錯誤優先風格”回調的形式:
```source-js
function foo(x,y,cb) {
setTimeout( function(){
// 假定 `cb(..)` 是“錯誤優先風格”
cb( null, x + y );
}, 1000 );
}
```
現在,我們將比較`thunkify(..)`和`promisify(..)`(也就是第三章的`Promise.wrap(..)`):
```source-js
// 對稱的:構建問題的回答者
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );
// 對稱的:提出問題
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );
// 取得 thunk 的回答
fooThunk( function(err,sum){
if (err) {
console.error( err );
}
else {
console.log( sum ); // 7
}
} );
// 取得 promise 的回答
fooPromise
.then(
function(sum){
console.log( sum ); // 7
},
function(err){
console.error( err );
}
);
```
thunkory和promisory實質上都是在問一個問題(一個值),thunk的`fooThunk`和promise的`fooPromise`分別代表這個問題的未來的答案。這樣看來,對稱性就清楚了。
帶著這個視角,我們可以看到為了異步而`yield`Promise的generator,也可以為異步而`yield`thunk。我們需要的只是一個更聰明的`run(..)`工具(就像以前一樣),它不僅可以尋找并連接一個被`yield`的Promise,而且可以給一個被`yield`的thunk提供回調。
考慮這段代碼:
```source-js
function *foo() {
var val = yield request( "http://some.url.1" );
console.log( val );
}
run( foo );
```
在這個例子中,`request(..)`既可以是一個返回一個promise的promisory,也可以是一個返回一個thunk的thunkory。從generator的內部代碼邏輯的角度看,我們不關心這個實現細節,這就它強大的地方!
所以,`request(..)`可以使以下任何一種形式:
```source-js
// promisory `request(..)` (見第三章)
var request = Promise.wrap( ajax );
// vs.
// thunkory `request(..)`
var request = thunkify( ajax );
```
最后,作為一個讓我們早先的`run(..)`工具支持thunk的補丁,我們可能會需要這樣的邏輯:
```source-js
// ..
// 我們收到了一個回調嗎?
else if (typeof next.value == "function") {
return new Promise( function(resolve,reject){
// 使用一個錯誤優先回調調用thunk
next.value( function(err,msg) {
if (err) {
reject( err );
}
else {
resolve( msg );
}
} );
} )
.then(
handleNext,
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
```
現在,我們generator既可以調用promisory來`yield`Promise,也可以調用thunkory來`yield`thunk,而不論那種情況,`run(..)`都將處理這個值并等待它的完成,以繼續generator。
在對稱性上,這兩個方式是看起來相同的。然而,我們應當指出這僅僅從Promise或thunk表示延續generator的未來值的角度講是成立的。
從更高的角度講,與Promise被設計成的那樣不同,thunk沒有提供,它們本身也幾乎沒有任何可靠性和可組合性的保證。在這種特定的generator異步模式下使用一個thunk作為Promise的替代品是可以工作的,但與Promise提供的所有好處相比,這應當被看做是一種次理想的方法。
如果你有選擇,那就偏向`yield pr`而非`yield th`。但是使`run(..)`工具可以處理兩種類型的值本身沒有什么問題。
注意:?在我們將要在附錄A中討論的,我的?*asynquence*?庫中的`runner(..)`工具,可以處理`yield`的Promise,thunk和?*asynquence*?序列。
## 前ES6時代的Generator
我希望你已經被說服了,generator是一個異步編程工具箱里的非常重要的增強工具。但它是ES6中的新語法,這意味著你不能像填補Promise(它只是新的API)那樣填補generator。那么如果我們不能奢望忽略前ES6時代的瀏覽器,我們該如何將generator帶到瀏覽器中呢?
對所有ES6中的新語法的擴展,有一些工具——稱呼他們最常見的名詞是轉譯器(transpilers),也就是轉換編譯器(trans-compilers)——它們會拿起你的ES6語法,并轉換為前ES6時代的等價代碼(但是明顯地變難看了!)。所以,generator可以被轉譯為具有相同行為但可以在ES5或以下版本進行工作的代碼。
但是怎么做到的?`yield`的“魔法”聽起來不像是那么容易轉譯的。在我們早先的基于閉包的?*迭代器*?例子中,實際上提示了一種解決方法。
### 手動變形
在我們討論轉譯器之前,讓我們延伸一下,在generator的情況下如何手動轉譯。這不僅是一個學院派的練習,因為這樣做實際上可以幫助我們進一步理解它們如何工作。
考慮這段代碼:
```source-js
// `request(..)` 是一個支持Promise的Ajax工具
function *foo(url) {
try {
console.log( "requesting:", url );
var val = yield request( url );
console.log( val );
}
catch (err) {
console.log( "Oops:", err );
return false;
}
}
var it = foo( "http://some.url.1" );
```
第一個要注意的事情是,我們仍然需要一個可以被調用的普通的`foo()`函數,而且它仍然需要返回一個?*迭代器*。那么讓我們來畫出非generator的變形草圖:
```source-js
function foo(url) {
// ..
// 制造并返回 iterator
return {
next: function(v) {
// ..
},
throw: function(e) {
// ..
}
};
}
var it = foo( "http://some.url.1" );
```
下一個需要注意的地方是,generator通過掛起它的作用域/狀態來施展它的“魔法”,但我們可以用函數閉包來模擬。為了理解如何寫出這樣的代碼,我們將先用狀態值注釋generator不同的部分:
```source-js
// `request(..)` 是一個支持Promise的Ajax工具
function *foo(url) {
// 狀態 *1*
try {
console.log( "requesting:", url );
var TMP1 = request( url );
// 狀態 *2*
var val = yield TMP1;
console.log( val );
}
catch (err) {
// 狀態 *3*
console.log( "Oops:", err );
return false;
}
}
```
注意:?為了更準去地講解,我們使用`TMP1`變量將`val = yield request..`語句分割為兩部分。`request(..)`發生在狀態`*1*`,而將完成值賦給`val`發生在狀態`*2*`。在我們將代碼轉換為非generator的等價物后,我們就可以擺脫中間的`TMP1`。
換句話所,`*1*`是初始狀態,`*2*`是`request(..)`成功的狀態,`*3*`是`request(..)`失敗的狀態。你可能會想象額外的`yield`步驟將如何編碼為額外的狀態。
回到我們被轉譯的generator,讓我們在這個閉包中定義一個變量`state`,用它來追蹤狀態:
```source-js
function foo(url) {
// 管理 generator 狀態
var state;
// ..
}
```
現在,讓我們在閉包內部定義一個稱為`process(..)`的內部函數,它用`switch`語句來處理各種狀態。
```source-js
// `request(..)` 是一個支持Promise的Ajax工具
function foo(url) {
// 管理 generator 狀態
var state;
// generator-范圍的變量聲明
var val;
function process(v) {
switch (state) {
case 1:
console.log( "requesting:", url );
return request( url );
case 2:
val = v;
console.log( val );
return;
case 3:
var err = v;
console.log( "Oops:", err );
return false;
}
}
// ..
}
```
在我們的generator中每種狀態都在`switch`語句中有它自己的`case`。每當我們需要處理一個新狀態時,`process(..)`就會被調用。我們一會就回來討論它如何工作。
對任何generator范圍的變量聲明(`val`),我們將它們移動到`process(..)`外面的`var`聲明中,這樣它們就可以在`process(..)`的多次調用中存活下來。但是“塊兒作用域”的`err`變量僅在`*3*`狀態下需要,所以我們將它留在原處。
在狀態`*1*`,與`yield request(..)`相反,我們`return request(..)`。在終結狀態`*2*`,沒有明確的`return`,所以我們僅僅`return;`也就是`return undefined`。在終結狀態`*3*`,有一個`return false`,我們保留它。
現在我們需要定義?*迭代器*?函數的代碼,以便人們恰當地調用`process(..)`:
```source-js
function foo(url) {
// 管理 generator 狀態
var state;
// generator-范圍的變量聲明
var val;
function process(v) {
switch (state) {
case 1:
console.log( "requesting:", url );
return request( url );
case 2:
val = v;
console.log( val );
return;
case 3:
var err = v;
console.log( "Oops:", err );
return false;
}
}
// 制造并返回 iterator
return {
next: function(v) {
// 初始狀態
if (!state) {
state = 1;
return {
done: false,
value: process()
};
}
// 成功地讓出繼續值
else if (state == 1) {
state = 2;
return {
done: true,
value: process( v )
};
}
// generator 已經完成了
else {
return {
done: true,
value: undefined
};
}
},
"throw": function(e) {
// 在狀態 *1* 中,有唯一明確的錯誤處理
if (state == 1) {
state = 3;
return {
done: true,
value: process( e )
};
}
// 否則,是一個不會被處理的錯誤,所以我們僅僅把它扔回去
else {
throw e;
}
}
};
}
```
這段代碼如何工作?
1. 第一個對?*迭代器*?的`next()`調用將把gtenerator從未初始化的狀態移動到狀態`1`,然后調用`process()`來處理這個狀態。`request(..)`的返回值是一個代表Ajax應答的promise,它作為`value`屬性從`next()`調用被返回。
2. 如果Ajax請求成功,第二個`next(..)`調用應當送進Ajax的應答值,它將我們的狀態移動到`2`。`process(..)`再次被調用(這次它被傳入Ajax應答的值),而從`next(..)`返回的`value`屬性將是`undefined`。
3. 然而,如果Ajax請求失敗,應當用錯誤調用`throw(..)`,它將狀態從`1`移動到`3`(而不是`2`)。`process(..)`再一次被調用,這詞被傳入了錯誤的值。這個`case`返回`false`,所以`false`作為`throw(..)`調用返回的`value`屬性。
從外面看——也就是僅僅與?*迭代器*?互動——這個普通的`foo(..)`函數與`*foo(..)`generator的工作方式是一樣的。所以我們有效地將ES6 generator“轉譯”為前ES6可兼容的!
然后我們就可以手動初始化我們的generator并控制它的迭代器——調用`var it = foo("..")`和`it.next(..)`等等——或更好地,我們可以將它傳遞給我們先前定義的`run(..)`工具,比如`run(foo,"..")`。
### 自動轉譯
前面的練習——手動編寫從ES6 generator到前ES6的等價物的變形過程——教會了我們generator在概念上是如何工作的。但是這種變形真的是錯綜復雜,而且不能很好地移植到我們代碼中的其他generator上。手動做這些工作是不切實際的,而且將會把generator的好處完全抵消掉。
但走運的是,已經存在幾種工具可以自動地將ES6 generator轉換為我們在前一節延伸出的東西。它們不僅幫我們做力氣活兒,還可以處理幾種我們敷衍而過的情況。
一個這樣的工具是regenerator(https://facebook.github.io/regenerator/ ),由Facebook的聰明伙計們開發的。
如果我們用regenerator來轉譯我們前面的generator,這就是產生的代碼(在編寫本文時):
```source-js
// `request(..)` 是一個支持Promise的Ajax工具
var foo = regeneratorRuntime.mark(function foo(url) {
var val;
return regeneratorRuntime.wrap(function foo$(context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.prev = 0;
console.log( "requesting:", url );
context$1$0.next = 4;
return request( url );
case 4:
val = context$1$0.sent;
console.log( val );
context$1$0.next = 12;
break;
case 8:
context$1$0.prev = 8;
context$1$0.t0 = context$1$0.catch(0);
console.log("Oops:", context$1$0.t0);
return context$1$0.abrupt("return", false);
case 12:
case "end":
return context$1$0.stop();
}
}, foo, this, [[0, 8]]);
});
```
這和我們的手動推導有明顯的相似性,比如`switch`/`case`語句,而且我們甚至可以看到,`val`被拉到了閉包外面,正如我們做的那樣。
當然,一個代價是這個generator的轉譯需要一個幫助工具庫`regeneratorRuntime`,它持有全部管理一個普通generator/*迭代器*所需的可復用邏輯。它的許多模板代碼看起來和我們的版本不同,但即便如此,概念還是可以看到的,比如使用`context$1$0.next = 4`追蹤generator的下一個狀態。
主要的結論是,generator不僅限于ES6+的環境中才有用。一旦你理解了它的概念,你可以在你的所有代碼中利用他們,并使用工具將代碼變形為舊環境兼容的。
這比使用`Promise`API的填補來實現前ES6的Promise要做更多的工作,但是努力完全是值得的,因為對于以一種可推理的,合理的,看似同步的順序風格來表達異步流程控制來說,generator實在是好太多了。
一旦你適應了generator,你將永遠不會回到面條般的回調地獄了!
## 復習
generator是一種ES6的新函數類型,它不像普通函數那樣運行至完成。相反,generator可以暫停在一種中間完成狀態(完整地保留它的狀態),而且它可以從暫停的地方重新開始。
這種暫停/繼續的互換是一種協作而非搶占,這意味著generator擁有的唯一能力是使用`yield`關鍵字暫停它自己,而且控制這個generator的?*迭代器*?擁有的唯一能力是繼續這個generator(通過`next(..)`)。
`yield`/`next(..)`的對偶不僅是一種控制機制,它實際上是一種雙向消息傳遞機制。一個`yield ..`表達式實質上為了等待一個值而暫停,而下一個`next(..)`調用將把值(或隱含的`undefined`)傳遞回這個暫停的`yield`表達式。
與異步流程控制關聯的generator的主要好處是,在一個generator內部的代碼以一種自然的同步/順序風格表達一個任務的各個步驟的序列。這其中的技巧是我們實質上將潛在的異步處理隱藏在`yield`關鍵字的后面——將異步處理移動到控制generator的?*迭代器*?代碼中。
換句話說,generator為異步代碼保留了順序的,同步的,阻塞的代碼模式,這允許我們的大腦更自然地推理代碼,解決了基于回調的異步產生的兩個關鍵問題中的一個。