特別說明,為便于查閱,文章轉自https://github.com/getify/You-Dont-Know-JS
在第二章中,我們發現了在使用回調表達異步流程控制時的兩個關鍵缺陷:
- 基于回調的異步與我們的大腦規劃任務的各個步驟的過程不相符。
- 由于 控制倒轉 回調是不可靠的,也是不可組合的。
在第三章中,我們詳細地討論了Promise如何反轉回調的 控制倒轉,重建了可靠性/可組合性。
現在讓我們把注意力集中到用一種順序的,看起來同步的風格來表達異步流程控制。使這一切成為可能的“魔法”是ES6的 generator。
打破運行至完成
在第一章中,我們講解了一個JS開發者們在他們的代碼中幾乎永恒依仗的一個認識:一旦函數開始執行,它將運行直至完成,沒有其他的代碼可以在運行期間干擾它。
這看起來可能很滑稽,ES6引入了一種新型的函數,它不按照“運行至完成”的行為進行動作。這種新型的函數稱為“generator(生成器)”。
為了理解它的含義,讓我們看看這個例子:
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代碼:
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()
的執行?
// 構建一個迭代器`it`來控制generator
var it = foo();
// 在這里開始`foo()`!
it.next();
x; // 2
bar();
x; // 3
it.next(); // x: 3
好了,這兩段代碼中有不少新的,可能使人困惑的東西,所以我們得跋涉好一段了。在我們用ES6的generator來講解不同的機制/語法之前,讓我們過一遍這個行為的流程:
-
it = foo()
操作 不會 執行*foo()
generator,它只不過構建了一個用來控制它執行的 迭代器(iterator)。我們一會更多地討論 迭代器。 - 第一個
it.next()
啟動了*foo()
generator,并且運行*foo()
第一行上的x++
。 -
*foo()
在yield
語句處暫停,就在這時第一個it.next()
調用結束。在這個時刻,*foo()
依然運行而且是活動的,但是處于暫停狀態。 - 我們觀察
x
的值,現在它是2
. - 我們調用
bar()
,它再一次用x++
遞增x
。 - 我們再一次觀察
x
的值,現在它是3
。 - 最后的
it.next()
調用使*foo()
generator從它暫停的地方繼續運行,而后運行使用x
的當前值3
的console.log(..)
語句。
清楚的是,*foo()
啟動了,但 沒有 運行到底——它停在yield
。我們稍后繼續*foo()
,讓它完成,但這甚至不是必須的。
所以,一個generator是一種函數,它可以開始和停止一次或多次,甚至沒必要一定要完成。雖然為什么它很強大看起來不那么明顯,但正如我們將要在本章剩下的部分將要講到的,它是我們用于在我們的代碼中構建“generator異步流程控制”模式的基礎構建塊兒之一。
輸入和輸出
一個generator函數是一種帶有我們剛才提到的新型處理模型的函數。但它仍然是一個函數,這意味著依舊有一些不變的基本原則——即,它依然接收參數(也就是“輸入”),而且它依然返回一個值(也就是“輸出”):
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(..)
實現的。
考慮下面的代碼:
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代碼:
var y = x * (yield);
return y;
這 第一個 yield
基本上是在 問一個問題:“我應該在這里插入什么值?”
誰來回答這個問題?好吧,第一個 next()
在這個時候已經為了啟動generator而運行過了,所以很明顯 它 不能回答這個問題。所以,第二個 next(..)
調用必須回答由 第一個 yield
提出的問題。
看到錯位了吧——第二個對第一個?
但是讓我們反轉一下我們的角度。讓我們不從generator的角度看問題,而從迭代器的角度看。
為了恰當地描述這種角度,我們還需要解釋一下,消息可以雙向發送——yield ..
作為表達式可以發送消息來應答next(..)
調用,而next(..)
可以發送值給暫停的yield
表達式。考慮一下這段稍稍調整過的代碼:
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運行期間 構成了一個雙向消息傳遞系統。
那么,如果只看 迭代器 代碼:
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的多個實例同時運行,它們甚至可以互動:
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在沒有輸入的情況下,從一些連接著的獨立資源中產生它自己的值。我們將在下一節中更多地討論產生值。
讓我們簡單地走一遍這個處理過程:
- 兩個
*foo()
在同時啟動,而且兩個next()
都分別從yield 2
語句中得到了2
的value
。 -
val2 * 10
就是2 * 10
,它被發送到第一個generator實例it1
,所以x
得到值20
。z
將1
遞增至2
,然后20 * 2
被yield
出來,將val1
設置為40
。 -
val1 * 5
就是40 * 5
,它被發送到第二個generator實例it2
中,所以x
得到值200
。z
又一次遞增,從2
到3
,然后200 * 3
被yield
出來,將val2
設置為600
。 -
val2 / 2
就是600 / 2
,它被發送到第一個generator實例it1
,所以y
得到值300
,然后分別為它的x y z
值打印出20 300 3
。 -
val1 / 4
就是40 / 4
,它被發送到第一個generator實例it2
,所以y
得到值10
,然后分別為它的x y z
值打印出200 10 3
。
這是在你腦海中跑過的一個“有趣”的例子。你還能保持清醒?
穿插
回想第一章中“運行至完成”一節的這個場景:
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,明確地穿插(甚至是在語句中間!)是可能的:
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(..)
的幫助函數,讓它控制 迭代器:
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()
之前全部完成(就像我們在第一章中做的那樣):
// 確保重置了`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
的值。
// 確保重置了`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
的值是什么嗎?不要作弊!
console.log( a, b ); // 12 18
注意: 作為留給讀者的練習,試試通過重新安排s1()
和s2()
調用的順序,看看你能得到多少種結果組合。別忘了你總是需要三個s1()
調用和四個s2()
調用。至于為什么,回想一下剛才關于使用yield
匹配next()
的討論。
當然,你幾乎不會想有意制造 這種 水平的,令人糊涂的穿插,因為他創建了非常難理解的代碼。但是這個練習很有趣,而且對于理解多個generator如何并發地運行在相同的共享作用域來說很有教育意義,因為會有一些地方這種能力十分有用。
我們會在本章末尾更詳細地討論generator并發。
生成值
在前一節中,我們提到了一個generator的有趣用法,作為一種生產值的方式。這 不是 我們本章主要關注的,但如果我們不在這里講一下基本我們會想念它的,特別是因為這種用法實質上是它的名稱的由來:生成器。
我們將要稍稍深入一下 迭代器 的話題,但我們會繞回到它們如何與generator關聯,并使用generator來 生成 值。
發生器與迭代器
想象你正在生產一系列的值,它們中的每一個都與前一個值有可定義的關系。為此,你將需要一個有狀態的發生器來記住上一個給出的值。
你可以用函數閉包(參加本系列的 作用域與閉包)來直接地實現這樣的東西:
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()
。
我們可以為我們的數字序列發生器實現標準的 迭代器;
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
循環,它意味著一個標準的 迭代器 可以使用原生的循環語法來自動地被消費:
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
條件來知道什么時候停止:
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
,也有默認的 迭代器:
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:
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
的代碼段中,你可能已經注意到了這一行:
[Symbol.iterator]: function(){ return this; }
這段有點讓人困惑的代碼制造了something
值——something
迭代器 的接口——也是一個 iterable;現在它既是一個 iterable 也是一個 迭代器。然后,我們把something
傳遞給for..of
循環:
for (var v of something) {
..
}
for..of
循環期待something
是一個 iterable,所以它會尋找并調用它的Symbol.iterator
函數。我們將這個函數定義為簡單地return this
,所以它將自己給出,而for..of
不會知道這些。
Generator迭代器
帶著 迭代器 的背景知識,讓我們把注意力移回generator。一個generator可以被看做一個值的發生器,我們通過一個 迭代器 接口的next()
調用每次從中抽取一個值。
所以,一個generator本身在技術上講并不是一個 iterable,雖然很相似——當你執行generator時,你就得到一個 迭代器:
function *foo(){ .. }
var it = foo();
我們可以用generator實現早前的something
無限數字序列發生器,就像這樣:
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了,而且你會看到它工作起來基本一模一樣:
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從外部被完成。這在你需要進行資源清理時很有用(數據庫連接等):
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的 迭代器 實例:
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要怎樣處理異步編碼模式,解決回調和類似的問題?讓我們開始回答這個重要的問題。
我們應當重溫一下第三章的一個場景。回想一下這個回調方式:
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表示相同的任務流控制,我們可以:
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的代碼段實際上要好 太多 了!但是這里有很多我們需要講解的。
首先,讓我們看看代碼的這一部分,也是最重要的部分:
var text = yield foo( 11, 31 );
console.log( text );
花一點時間考慮一下這段代碼如何工作。我們調用了一個普通的函數foo(..)
,而且我們顯然可以從Ajax調用那里得到text
,即便它是異步的。
這怎么可能?如果你回憶一下第一章的最開始,我們有一個幾乎完全一樣的代碼:
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請求成功,我們調用:
it.next( data );
這將使generator使用應答數據繼續運行,這意味著我們暫停的yield
表達式直接收到這個值,然后因為它重新開始以運行generator代碼,所以這個值被賦給本地變量text
。
很酷吧?
退一步考慮一下它的意義。我們在generator內部的代碼看起來完全是同步的(除了yield
關鍵字本身),但隱藏在幕后的是,在foo(..)
內部,操作可以完全是異步的。
這很偉大! 這幾乎完美地解決了我們前面遇到的問題:回調不能像我們的大腦可以關聯的那樣,以一種順序,同步的風格表達異步處理。
實質上,我們將異步處理作為實現細節抽象出去,以至于我們可以同步地/順序地推理我們的流程控制:“發起Ajax請求,然后在它完成之后打印應答。” 當然,我們僅僅在這個流程控制中表達了兩個步驟,但同樣的能力可以無邊界地延伸,讓我們需要表達多少步驟,就表達多少。
提示: 這是一個如此重要的認識,為了充分理解,現在回過頭去再把最后三段讀一遍!
同步錯誤處理
但是前面的generator代碼會 讓 出更多的好處給我們。讓我們把注意力移到generator內部的try..catch
上:
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中:
if (err) {
// 向`*main()`中扔進一個錯誤
it.throw( err );
}
generator的yield
暫停特性不僅意味著我們可以從異步的函數調用那里得到看起來同步的return
值,還意味著我們可以同步地捕獲這些異步函數調用的錯誤!
那么我們看到了,我們可以將錯誤 拋入 generator,但是將錯誤 拋出 一個generator呢?和你期望的一樣:
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沒處理,那么 迭代器 代碼必須處理它:
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的例子:
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()
放在一起:
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。我們從手動這么做開始:
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(..)
的獨立工具:
// 感謝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()
一起使用呢?
function *main() {
// ..
}
run( main );
就是這樣!按照我們連接run(..)
的方式,它將自動地,異步地推進你傳入的generator,直到完成。
注意: 我們定義的run(..)
返回一個promise,它被連接成一旦generator完成就立即解析,或者收到一個未捕獲的異常,而generator沒有處理它。我們沒有在這里展示這種能力,但我們會在本章稍后回到這個話題。
ES7: async
和 await
?
前面的模式——generator讓出一個Promise,然后這個Promise控制generator的 迭代器 向前推進至它完成——是一個如此強大和有用的方法,如果我們能不通過亂七八糟的幫助工具庫(也就是run(..)
)來使用它就更好了。
在這方面可能有一些好消息。在寫作這本書的時候,后ES6,ES7化的時間表上已經出現了草案,對這個問題提供早期但強大的附加語法支持。顯然,現在還太早而不能保證其細節,但是有相當大的可能性它將蛻變為類似于下面的東西:
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化草案已經存在,并且有了早期的支持和熱忱的擁護。這一事實為這種異步模式在未來的重要性上信心滿滿地投了有力的一票。