感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運大獎:點擊這里領(lǐng)取
在第二章中,我們發(fā)現(xiàn)了在使用回調(diào)表達異步流程控制時的兩個關(guān)鍵缺陷:
- 基于回調(diào)的異步與我們的大腦規(guī)劃任務(wù)的各個步驟的過程不相符。
- 由于 控制倒轉(zhuǎn) 回調(diào)是不可靠的,也是不可組合的。
在第三章中,我們詳細地討論了Promise如何反轉(zhuǎn)回調(diào)的 控制倒轉(zhuǎn),重建了可靠性/可組合性。
現(xiàn)在讓我們把注意力集中到用一種順序的,看起來同步的風格來表達異步流程控制。使這一切成為可能的“魔法”是ES6的 generator。
打破運行至完成
在第一章中,我們講解了一個JS開發(fā)者們在他們的代碼中幾乎永恒依仗的一個認識:一旦函數(shù)開始執(zhí)行,它將運行直至完成,沒有其他的代碼可以在運行期間干擾它。
這看起來可能很滑稽,ES6引入了一種新型的函數(shù),它不按照“運行至完成”的行為進行動作。這種新型的函數(shù)稱為“generator(生成器)”。
為了理解它的含義,然我們看看這個例子:
var x = 1;
function foo() {
x++;
bar(); // <-- 這一行會發(fā)生什么?
console.log( "x:", x );
}
function bar() {
x++;
}
foo(); // x: 3
在這個例子中,我們確信bar()
會在x++
和console.log(x)
之間運行。但如果bar()
不在這里呢?很明顯結(jié)果將是2
而不是3
。
現(xiàn)在讓我們來燃燒你的大腦。要是bar()
不存在,但以某種方式依然可以在x++
和console.log(x)
語句之間運行呢?這可能嗎?
在 搶占式(preemptive) 多線程語言中,bar()
去“干擾”并正好在兩個語句之間那一時刻運行,實質(zhì)上時可能的。但JS不是搶占式的,也(還)不是多線程的。但是,如果foo()
本身可以用某種辦法在代碼的這一部分指示一個“暫停”,那么這種“干擾”(并發(fā))的 協(xié)作 形式就是可能的。
注意: 我使用“協(xié)作”這個詞,不僅是因為它與經(jīng)典的并發(fā)術(shù)語有關(guān)聯(lián)(見第一章),也因為正如你將在下一個代碼段中看到的,ES6在代碼中指示暫停點的語法是yield
——暗示一個讓出控制權(quán)的禮貌的 協(xié)作。
這就是實現(xiàn)這種協(xié)作并發(fā)的ES6代碼:
var x = 1;
function *foo() {
x++;
yield; // 暫停!
console.log( "x:", x );
}
function bar() {
x++;
}
注意: 你將很可能在大多數(shù)其他的JS文檔/代碼中看到,一個generator的聲明被格式化為function* foo() { .. }
而不是我在這里使用的function *foo() { .. }
——唯一的區(qū)別是擺放*
位置的風格。這兩種形式在功能性/語法上是完全一樣的,還有第三種function*foo() { .. }
(沒空格)形式。這兩種風格存在爭議,但我基本上偏好function *foo..
,因為當我在寫作中用*foo()
引用一個generator時,這種形式可以匹配我寫的東西。如果我只說foo()
,你就不會清楚地知道我是在說一個generator還是一個一般的函數(shù)。這純粹是一個風格偏好的問題。
現(xiàn)在,我們該如何運行上面的代碼,使bar()
在yield
那一點取代*foo()
的執(zhí)行?
// 構(gòu)建一個迭代器`it`來控制generator
var it = foo();
// 在這里開始`foo()`!
it.next();
x; // 2
bar();
x; // 3
it.next(); // x: 3
好了,這兩段代碼中有不少新的,可能使人困惑的東西,所以我們得跋涉好一段了。在我們用ES6的generator來講解不同的機制/語法之前,讓我們過一遍這個行為的流程:
-
it = foo()
操作 不會 執(zhí)行*foo()
generator,它只不過構(gòu)建了一個用來控制它執(zhí)行的 迭代器(iterator)。我們一會更多地討論 迭代器。 - 第一個
it.next()
啟動了*foo()
generator,并且運行*foo()
第一行上的x++
。 -
*foo()
在yield
語句處暫停,就在這時第一個it.next()
調(diào)用結(jié)束。在這個時刻,*foo()
依然運行而且是活動的,但是處于暫停狀態(tài)。 - 我們觀察
x
的值,現(xiàn)在它是2
. - 我們調(diào)用
bar()
,它再一次用x++
遞增x
。 - 我們再一次觀察
x
的值,現(xiàn)在它是3
。 - 最后的
it.next()
調(diào)用使*foo()
generator從它暫停的地方繼續(xù)運行,而后運行使用x
的當前值3
的console.log(..)
語句。
清楚的是,*foo()
啟動了,但 沒有 運行到底——它停在yield
。我們稍后繼續(xù)*foo()
,讓它完成,但這甚至不是必須的。
所以,一個generator是一種函數(shù),它可以開始和停止一次或多次,甚至沒必要一定要完成。雖然為什么它很強大看起來不那么明顯,但正如我們將要在本章剩下的部分將要講到的,它是我們用于在我們的代碼中構(gòu)建“generator異步流程控制”模式的基礎(chǔ)構(gòu)建塊兒之一。
輸入和輸出
一個generator函數(shù)是一種帶有我們剛才提到的新型處理模型的函數(shù)。但它仍然是一個函數(shù),這意味著依舊有一些不變的基本原則——即,它依然接收參數(shù)(也就是“輸入”),而且它依然返回一個值(也就是“輸出”):
function *foo(x,y) {
return x * y;
}
var it = foo( 6, 7 );
var res = it.next();
res.value; // 42
我們將6
和7
分別作為參數(shù)x
和y
傳遞給*foo(..)
。而*foo(..)
將值42
返回給調(diào)用端代碼。
現(xiàn)在我們可以看到發(fā)生器的調(diào)用和一般函數(shù)的調(diào)用的一個不同之處了。foo(6,7)
顯然看起來很熟悉。但微妙的是,*foo(..)
generator不會像一個函數(shù)那樣實際運行起來。
相反,我們只是創(chuàng)建了 迭代器 對象,將它賦值給變量it
,來控制*foo(..)
generator。當我們調(diào)用it.next()
時,它指示*foo(..)
generator從現(xiàn)在的位置向前推進,直到下一個yield
或者generator的最后。
next(..)
調(diào)用的結(jié)果是一個帶有value
屬性的對象,它持有從*foo(..)
返回的任何值(如果有的話)。換句話說,yield
導致在generator運行期間,一個值被從中發(fā)送出來,有點兒像一個中間的return
。
但是,為什么我們需要這個完全間接的 迭代器 對象來控制generator還不清楚。我們回頭會討論它的,我保證。
迭代通信
generator除了接收參數(shù)和擁有返回值,它們還內(nèi)建有更強大,更吸引人的輸入/輸出消息能力,這是通過使用yield
和next(..)
實現(xiàn)的。
考慮下面的代碼:
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
作為參數(shù)x
傳入。之后我們調(diào)用it.next()
,它啟動了*foo(..)
.
在*foo(..)
內(nèi)部,var y = x ..
語句開始被處理,但它運行到了一個yield
表達式。就在這時,它暫停了*foo(..)
(就在賦值語句的中間!),而且請求調(diào)用端代碼為yield
表達式提供一個結(jié)果值。接下來,我們調(diào)用it.next(7)
,將7
這個值傳回去作為暫停的yield
表達式的結(jié)果。
所以,在這個時候,賦值語句實質(zhì)上是var y = 6 * 7
。現(xiàn)在,return y
將值42
作為結(jié)果返回給it.next( 7 )
調(diào)用。
注意一個非常重要,而且即便是對于老練的JS開發(fā)者也非常容易犯糊涂的事情:根據(jù)你的角度,在yield
和next(..)
調(diào)用之間存在著錯位。一般來說,你所擁有的next(..)
調(diào)用的數(shù)量,會比你所擁有的yield
語句的數(shù)量多一個——前面的代碼段中有一個yield
和兩個next(..)
調(diào)用。
為什么會有這樣的錯位?
因為第一個next(..)
總是啟動一個generator,然后運行至第一個yield
。但是第二個next(..)
調(diào)用滿足了第一個暫停的yield
表達式,而第三個next(..)
將滿足第二個yield
,如此反復。
兩個疑問的故事
實際上,你主要考慮的是哪部分代碼會影響你是否感知到錯位。
僅考慮generator代碼:
var y = x * (yield);
return y;
這 第一個 yield
基本上是在 問一個問題:“我應(yīng)該在這里插入什么值?”
誰來回答這個問題?好吧,第一個 next()
在這個時候已經(jīng)為了啟動generator而運行過了,所以很明顯 它 不能回答這個問題。所以,第二個 next(..)
調(diào)用必須回答由 第一個 yield
提出的問題。
看到錯位了吧——第二個對第一個?
但是讓我們反轉(zhuǎn)一下我們的角度。讓我們不從generator的角度看問題,而從迭代器的角度看。
為了恰當?shù)孛枋鲞@種角度,我們還需要解釋一下,消息可以雙向發(fā)送——yield ..
作為表達式可以發(fā)送消息來應(yīng)答next(..)
調(diào)用,而next(..)
可以發(fā)送值給暫停的yield
表達式。考慮一下這段稍稍調(diào)整過的代碼:
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運行期間 構(gòu)成了一個雙向消息傳遞系統(tǒng)。
那么,如果只看 迭代器 代碼:
var res = it.next(); // 第一個`next()`,不傳遞任何東西
res.value; // "Hello"
res = it.next( 7 ); // 傳遞`7`給等待中的`yield`
res.value; // 42
注意: 我們沒有傳遞任何值給第一個next()
調(diào)用,而且是故意的。只有一個暫停的yield
才能接收這樣一個被next(..)
傳遞的值,但是當我們調(diào)用第一個next()
時,在generator的最開始并 沒有任何暫停的yield
可以接收這樣的值。語言規(guī)范和所有兼容此語言規(guī)范的瀏覽器只會無聲地 丟棄 任何傳入第一個next()
的東西。傳遞這樣的值是一個壞主意,因為你只不過創(chuàng)建了一些令人困惑的無聲“失敗”的代碼。所以,記得總是用一個無參數(shù)的next()
來啟動generator。
第一個next()
調(diào)用(沒有任何參數(shù)的)基本上是在 問一個問題:“*foo(..)
generator將要給我的 下一個 值是什么?”,誰來回答這個問題?第一個yield
表達式。
看到了?這里沒有錯位。
根據(jù)你認為是 誰 在問問題,在yield
和next(..)
之間的錯位既存在又不存在。
但等一下!跟yield
語句的數(shù)量比起來,還有一個額外的next()
。那么,這個最后的it.next(7)
調(diào)用又一次在詢問generator 下一個 產(chǎn)生的值是什么。但是沒有yield
語句剩下可以回答了,不是嗎?那么誰來回答?
return
語句回答這個問題!
而且如果在你的generator中 沒有return
——比起一般的函數(shù),generator中的return
當然不再是必須的——總會有一個假定/隱式的return;
(也就是return undefined;
),它默認的目的就是回答由最后的it.next(7)
調(diào)用 提出 的問題。
這些問題與回答——用yield
和next(..)
進行雙向消息傳遞——十分強大,但還是看不出來這些機制與異步流程控制有什么聯(lián)系。我們正在接近真相!
多迭代器
從語法使用上來看,當你用一個 迭代器 來控制generator時,你正在控制聲明的generator函數(shù)本身。但這里有一個容易忽視的微妙細節(jié):每當你構(gòu)建一個 迭代器,你都隱含地構(gòu)建了一個將由這個 迭代器 控制的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的多個并發(fā)運行實例的最常見的用法,不是這樣的互動,而是generator在沒有輸入的情況下,從一些連接著的獨立資源中產(chǎn)生它自己的值。我們將在下一節(jié)中更多地討論產(chǎn)生值。
讓我們簡單地走一遍這個處理過程:
- 兩個
*foo()
在同時啟動,而且兩個next()
都分別從yield 2
語句中得到了2
的value
。 -
val2 * 10
就是2 * 10
,它被發(fā)送到第一個generator實例it1
,所以x
得到值20
。z
將1
遞增至2
,然后20 * 2
被yield
出來,將val1
設(shè)置為40
。 -
val1 * 5
就是40 * 5
,它被發(fā)送到第二個generator實例it2
中,所以x
得到值200
。z
又一次遞增,從2
到3
,然后200 * 3
被yield
出來,將val2
設(shè)置為600
。 -
val2 / 2
就是600 / 2
,它被發(fā)送到第一個generator實例it1
,所以y
得到值300
,然后分別為它的x y z
值打印出20 300 3
。 -
val1 / 4
就是40 / 4
,它被發(fā)送到第一個generator實例it2
,所以y
得到值10
,然后分別為它的x y z
值打印出200 10 3
。
這是在你腦海中跑過的一個“有趣”的例子。你還能保持清醒?
穿插
回想第一章中“運行至完成”一節(jié)的這個場景:
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函數(shù),當然要么是foo()
可以首先運行完成,要么是bar()
可以首先運行至完成,但是foo()
不可能與bar()
穿插它的獨立語句。所以,前面這段代碼只有兩個可能的結(jié)果。
然而,使用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);
}
根據(jù) 迭代器 控制*foo()
與*bar()
分別以什么樣的順序被調(diào)用,前面這段代碼可以產(chǎn)生幾種不同的結(jié)果。換句話說,通過兩個generator在同一個共享的變量上穿插,我們實際上可以展示(以一種模擬的方式)在第一章中討論的,理論上的“線程的競合狀態(tài)”環(huán)境。
首先,讓我們制造一個稱為step(..)
的幫助函數(shù),讓它控制 迭代器:
function step(gen) {
var it = gen();
var last;
return function() {
// 不論`yield`出什么,只管在下一次時直接把它塞回去!
last = it.next( last ).value;
};
}
step(..)
初始化一個generator來創(chuàng)建它的it
迭代器,然后它返回一個函數(shù),每次這個函數(shù)被調(diào)用時,都將 迭代器 向前推一步。另外,前一個被yield
出來的值將被直接發(fā)給下一步。所以,yield 8
將變成8
而yield b
將成為b
(不管它在yield
時是什么值)。
現(xiàn)在,為了好玩兒,讓我們做一些實驗,來看看將這些*foo()
與*bar()
的不同塊兒穿插時的效果。我們從一個無聊的基本情況開始,保證*foo()
在*bar()
之前全部完成(就像我們在第一章中做的那樣):
// 確保重置了`a`和`b`
a = 1;
b = 2;
var s1 = step( foo );
var s2 = step( bar );
// 首先完全運行`*foo()`
s1();
s1();
s1();
// 現(xiàn)在運行`*bar()`
s2();
s2();
s2();
s2();
console.log( a, b ); // 11 22
最終結(jié)果是11
和22
,就像第一章的版本那樣。現(xiàn)在讓我們把順序混合穿插,來看看它如何改變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;
在我告訴你結(jié)果之前,你能指出在前面的程序運行之后a
和b
的值是什么嗎?不要作弊!
console.log( a, b ); // 12 18
注意: 作為留給讀者的練習,試試通過重新安排s1()
和s2()
調(diào)用的順序,看看你能得到多少種結(jié)果組合。別忘了你總是需要三個s1()
調(diào)用和四個s2()
調(diào)用。至于為什么,回想一下剛才關(guān)于使用yield
匹配next()
的討論。
當然,你幾乎不會想有意制造 這種 水平的,令人糊涂的穿插,因為他創(chuàng)建了非常難理解的代碼。但是這個練習很有趣,而且對于理解多個generator如何并發(fā)地運行在相同的共享作用域來說很有教育意義,因為會有一些地方這種能力十分有用。
我們會在本章末尾更詳細地討論generator并發(fā)。
生成值
在前一節(jié)中,我們提到了一個generator的有趣用法,作為一種生產(chǎn)值的方式。這 不是 我們本章主要關(guān)注的,但如果我們不在這里講一下基本我們會想念它的,特別是因為這種用法實質(zhì)上是它的名稱的由來:生成器。
我們將要稍稍深入一下 迭代器 的話題,但我們會繞回到它們?nèi)绾闻cgenerator關(guān)聯(lián),并使用generator來 生成 值。
發(fā)生器與迭代器
想象你正在生產(chǎn)一系列的值,它們中的每一個都與前一個值有可定義的關(guān)系。為此,你將需要一個有狀態(tài)的發(fā)生器來記住上一個給出的值。
你可以用函數(shù)閉包(參加本系列的 作用域與閉包)來直接地實現(xiàn)這樣的東西:
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
的計算邏輯已經(jīng)被簡化了,但從概念上講,直到 下一次 gimmeSomething()
調(diào)用發(fā)生之前,我們不想計算 下一個值(也就是nextVal
),因為一般對于持久性更強的,或者比簡單的number
更有限的資源的發(fā)生器來說,那可能是一種資源泄漏的設(shè)計。
生成隨意的數(shù)字序列不是是一個很真實的例子。但是如果你從一個數(shù)據(jù)源中生成記錄呢?你可以想象很多相同的代碼。
事實上,這種任務(wù)是一種非常常見的設(shè)計模式,通常用迭代器解決。一個 迭代器 是一個明確定義的接口,用來逐個通過一系列從發(fā)生器得到的值。迭代器的JS接口,和大多數(shù)語言一樣,是在你每次想從發(fā)生器中得到下一個值時調(diào)用的next()
。
我們可以為我們的數(shù)字序列發(fā)生器實現(xiàn)標準的 迭代器;
var something = (function(){
var nextVal;
return {
// `for..of`循環(huán)需要這個
[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”一節(jié)中講解為什么我們在這個代碼段中需要[Symbol.iterator]: ..
這一部分。在語法上講,兩個ES6特性在發(fā)揮作用。首先,[ .. ]
語法稱為一個 計算型屬性名(參見本系列的 this與對象原型)。它是一種字面對象定義方法,用來指定一個表達式并使用這個表達式的結(jié)果作為屬性名。另一個,Symbol.iterator
是ES6預(yù)定義的特殊Symbol
值。
next()
調(diào)用返回一個對象,它帶有兩個屬性:done
是一個boolean
值表示 迭代器 的完成狀態(tài);value
持有迭代的值。
ES6還增加了for..of
循環(huán),它意味著一個標準的 迭代器 可以使用原生的循環(huán)語法來自動地被消費:
for (var v of something) {
console.log( v );
// 不要讓循環(huán)永無休止!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
注意: 因為我們的something
迭代器總是返回done:false
,這個for..of
循環(huán)將會永遠運行,這就是為什么我們條件性地放進一個break
。對于迭代器來說永不終結(jié)是完全沒有問題的,但是也有一些情況 迭代器 將運行在有限的值的集合上,而最終返回done:true
。
for..of
循環(huán)為每一次迭代自動調(diào)用next()
——他不會給next()
傳入任何值——而且他將會在收到一個done:true
時自動終結(jié)。這對于在一個集合的數(shù)據(jù)中進行循環(huán)十分方便。
當然,你可以手動循環(huán)一個迭代器,調(diào)用next()
并檢查done:true
條件來知道什么時候停止:
for (
var ret;
(ret = something.next()) && !ret.done;
) {
console.log( ret.value );
// 不要讓循環(huán)永無休止!
if (ret.value > 500) {
break;
}
}
// 1 9 33 105 321 969
注意: 這種手動的for
方式當然要比ES6的for..of
循環(huán)語法難看,但它的好處是它提供給你一個機會,在有必要時傳值給next(..)
調(diào)用。
除了制造你自己的 迭代器 之外,許多JS中(就ES6來說)內(nèi)建的數(shù)據(jù)結(jié)構(gòu),比如array
,也有默認的 迭代器:
var a = [1,3,5,7,9];
for (var v of a) {
console.log( v );
}
// 1 3 5 7 9
for..of
循環(huán)向a
要來它的迭代器,并自動使用它迭代a
的值。
注意: 看起來像是一個ES6的奇怪省略,普通的object
有意地不帶有像array
那樣的默認 迭代器。原因比我們要在這里講的深刻得多。如果你想要的只是迭代一個對象的屬性(不特別保證順序),Object.keys(..)
返回一個array
,它可以像for (var k of Object.keys(obj)) { ..
這樣使用。像這樣用for..of
循環(huán)一個對象上的鍵,與用for..in
循環(huán)內(nèi)很相似,除了在for..in
中會包含[[Prototype]]
鏈的屬性,而Object.keys(..)
不會(參見本系列的 this與對象原型)。
Iterables
在我們運行的例子中的something
對象被稱為一個 迭代器,因為它的接口中有next()
方法。但一個緊密關(guān)聯(lián)的術(shù)語是 iterable,它指 包含有 一個可以迭代它所有值的迭代器的對象。
在ES6中,從一個 iterable 中取得一個 迭代器 的方法是,iterable 上必須有一個函數(shù),它的名稱是特殊的ES6符號值Symbol.iterator
。當這個函數(shù)被調(diào)用時,它就會返回一個 迭代器。雖然不是必須的,但一般來說每次調(diào)用應(yīng)當返回一個全新的 迭代器。
前一個代碼段的a
就是一個 iterable。for..of
循環(huán)自動地調(diào)用它的Symbol.iterator
函數(shù)來構(gòu)建一個 迭代器。我們當然可以手動地調(diào)用這個函數(shù),然后使用它返回的 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
的代碼段中,你可能已經(jīng)注意到了這一行:
[Symbol.iterator]: function(){ return this; }
這段有點讓人困惑的代碼制造了something
值——something
迭代器 的接口——也是一個 iterable;現(xiàn)在它既是一個 iterable 也是一個 迭代器。然后,我們把something
傳遞給for..of
循環(huán):
for (var v of something) {
..
}
for..of
循環(huán)期待something
是一個 iterable,所以它會尋找并調(diào)用它的Symbol.iterator
函數(shù)。我們將這個函數(shù)定義為簡單地return this
,所以它將自己給出,而for..of
不會知道這些。
Generator迭代器
帶著 迭代器 的背景知識,讓我們把注意力移回generator。一個generator可以被看做一個值的發(fā)生器,我們通過一個 迭代器 接口的next()
調(diào)用每次從中抽取一個值。
所以,一個generator本身在技術(shù)上講并不是一個 iterable,雖然很相似——當你執(zhí)行g(shù)enerator時,你就得到一個 迭代器:
function *foo(){ .. }
var it = foo();
我們可以用generator實現(xiàn)早前的something
無限數(shù)字序列發(fā)生器,就像這樣:
function *something() {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
注意: 在一個真實的JS程序中含有一個while..true
循環(huán)通常是一件非常不好的事情,至少如果它沒有一個break
或return
語句,那么它就很可能永遠運行,并同步地,阻塞/鎖定瀏覽器UI。然而,在generator中,如果這樣的循環(huán)含有一個yield
,那它就是完全沒有問題的,因為generator將在每次迭代后暫停,yield
回主程序和/或事件輪詢隊列。說的明白點兒,“generator把while..true
帶回到JS編程中了!”
這變得相當干凈和簡單點兒了,對吧?因為generator會暫停在每個yield
,*something()
函數(shù)的狀態(tài)(作用域)被保持著,這意味著沒有必要用閉包的模板代碼來跨調(diào)用保留變量的狀態(tài)了。
不僅是更簡單的代碼——我們不必自己制造 迭代器 接口了——它實際上是更合理的代碼,因為它更清晰地表達了意圖。比如,while..true
循環(huán)告訴我們這個generator將要永遠運行——只要我們一直向它請求,它就一直 產(chǎn)生 值。
現(xiàn)在我們可以在for..of
循環(huán)中使用新得發(fā)亮的*something()
generator了,而且你會看到它工作起來基本一模一樣:
for (var v of something()) {
console.log( v );
// 不要讓循環(huán)永無休止!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
不要跳過for (var v of something()) ..
!我們不僅僅像之前的例子那樣將something
作為一個值引用了,而是調(diào)用*something()
generator來得到它的 迭代器,并交給for..of
使用。
如果你仔細觀察,在這個generator和循環(huán)的互動中,你可能會有兩個疑問:
- 為什么我們不能說
for (var v of something) ..
?因為這個something
是一個generator,而不是一個 iterable。我們不得不調(diào)用something()
來構(gòu)建一個發(fā)生器給for..of
,以便它可以迭代。 -
something()
調(diào)用創(chuàng)建一個 迭代器,但是for..of
想要一個 iterable,對吧?對,generator的 迭代器 上也有一個Symbol.iterator
函數(shù),這個函數(shù)基本上就是return this
,就像我們剛才定義的something
iterable。換句話說generator的 迭代器 也是一個 iterable!
停止Generator
在前一個例子中,看起來在循環(huán)的break
被調(diào)用后,*something()
generator的 迭代器 實例基本上被留在了一個永遠掛起的狀態(tài)。
但是這里有一個隱藏的行為為你處理這件事。for..of
循環(huán)的“異常完成”(“提前終結(jié)”等等)——一般是由break
,return
,或未捕捉的異常導致的——會向generator的 迭代器 發(fā)送一個信號,以使它終結(jié)。
注意: 技術(shù)上講,for..of
循環(huán)也會在循環(huán)正常完成時向 迭代器 發(fā)送這個信號。對于generator來說,這實質(zhì)上是一個無實際意義的操作,因為generator的 迭代器 要首先完成,for..of
循環(huán)才能完成。然而,自定義的 迭代器 可能會希望從for..of
循環(huán)的消費者那里得到另外的信號。
雖然一個for..of
循環(huán)將會自動發(fā)送這種信號,你可能會希望手動發(fā)送信號給一個 迭代器;你可以通過調(diào)用return(..)
來這么做。
如果你在generator內(nèi)部指定一個try..finally
從句,它將總是被執(zhí)行,即便是generator從外部被完成。這在你需要進行資源清理時很有用(數(shù)據(jù)庫連接等):
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
的例子將會觸發(fā)finally
從句。但是你可以用return(..)
從外部來手動終結(jié)generator的 迭代器 實例:
var it = something();
for (var v of it) {
console.log( v );
// 不要讓循環(huán)永無休止!
if (v > 500) {
console.log(
// 使generator得迭代器完成
it.return( "Hello World" ).value
);
// 這里不需要`break`
}
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World
當我們調(diào)用it.return(..)
時,它會立即終結(jié)generator,從而運行finally
從句。而且,它會將返回的value
設(shè)置為你傳入return(..)
的任何東西,這就是Hellow World
如何立即返回來的。我們現(xiàn)在也不必再包含一個break
,因為generator的 迭代器 會被設(shè)置為done:true
,所以for..of
循環(huán)會在下一次迭代時終結(jié)。
generator的命名大部分源自于這種 消費生產(chǎn)的值 的用法。但要重申的是,這只是generator的用法之一,而且坦白的說,在這本書的背景下這甚至不是我們主要關(guān)注的。
但是現(xiàn)在我們更加全面地了解它們的機制是如何工作的,我們接下來可以將注意力轉(zhuǎn)向generator如何實施于異步并發(fā)。
異步地迭代Generator
generator要怎樣處理異步編碼模式,解決回調(diào)和類似的問題?讓我們開始回答這個重要的問題。
我們應(yīng)當重溫一下第三章的一個場景。回想一下這個回調(diào)方式:
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表示相同的任務(wù)流控制,我們可以:
function foo(x,y) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
function(err,data){
if (err) {
// 向`*main()`中扔進一個錯誤
it.throw( err );
}
else {
// 使用收到的`data`來繼續(xù)`*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();
一眼看上去,這個代碼段要比以前的回調(diào)代碼更長,而且也許看起來更復雜。但不要讓這種印象誤導你。generator的代碼段實際上要好 太多 了!但是這里有很多我們需要講解的。
首先,讓我們看看代碼的這一部分,也是最重要的部分:
var text = yield foo( 11, 31 );
console.log( text );
花一點時間考慮一下這段代碼如何工作。我們調(diào)用了一個普通的函數(shù)foo(..)
,而且我們顯然可以從Ajax調(diào)用那里得到text
,即便它是異步的。
這怎么可能?如果你回憶一下第一章的最開始,我們有一個幾乎完全一樣的代碼:
var data = ajax( "..url 1.." );
console.log( data );
但是這段代碼不好用!你能發(fā)現(xiàn)不同嗎?它就是在generator中使用的yield
。
這就是魔法發(fā)生的地方!是它允許我們擁有一個看起來是阻塞的,同步的,但實際上不會阻塞整個程序的代碼;它僅僅暫停/阻塞在generator本身的代碼。
在yield foo(11,31)
中,首先foo(11,31)
調(diào)用被發(fā)起,它什么也不返回(也就是undefined
),所以我們發(fā)起了數(shù)據(jù)請求,然后我們實際上做的是yield undefined
。這沒問題,因為這段代碼現(xiàn)在沒有依賴yield
的值來做任何有趣的事。我們在本章稍后再重新討論這個問題。
在這里,我們沒有將yield
作為消息傳遞的工具,只是作為進行暫停/阻塞的流程控制的工具。實際上,它會傳遞消息,但是只是單向的,在generator被繼續(xù)運行之后。
那么,generator暫停在了yield
,它實質(zhì)上再問一個問題,“我該將什么值返回并賦給變量text
?”誰來回答這個問題?
看一下foo(..)
。如果Ajax請求成功,我們調(diào)用:
it.next( data );
這將使generator使用應(yīng)答數(shù)據(jù)繼續(xù)運行,這意味著我們暫停的yield
表達式直接收到這個值,然后因為它重新開始以運行g(shù)enerator代碼,所以這個值被賦給本地變量text
。
很酷吧?
退一步考慮一下它的意義。我們在generator內(nèi)部的代碼看起來完全是同步的(除了yield
關(guān)鍵字本身),但隱藏在幕后的是,在foo(..)
內(nèi)部,操作可以完全是異步的。
這很偉大! 這幾乎完美地解決了我們前面遇到的問題:回調(diào)不能像我們的大腦可以關(guān)聯(lián)的那樣,以一種順序,同步的風格表達異步處理。
實質(zhì)上,我們將異步處理作為實現(xiàn)細節(jié)抽象出去,以至于我們可以同步地/順序地推理我們的流程控制:“發(fā)起Ajax請求,然后在它完成之后打印應(yīng)答。” 當然,我們僅僅在這個流程控制中表達了兩個步驟,但同樣的能力可以無邊界地延伸,讓我們需要表達多少步驟,就表達多少。
提示: 這是一個如此重要的認識,為了充分理解,現(xiàn)在回過頭去再把最后三段讀一遍!
同步錯誤處理
但是前面的generator代碼會 讓 出更多的好處給我們。讓我們把注意力移到generator內(nèi)部的try..catch
上:
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
這是怎么工作的?foo(..)
調(diào)用是異步完成的,try..catch
不是無法捕捉異步錯誤嗎?就像我們在第三章中看到的?
我們已經(jīng)看到了yield
如何讓賦值語句暫停,來等待foo(..)
去完成,以至于完成的響應(yīng)可以被賦予text
。牛X的是,yield
暫停 還 允許generator來catch
一個錯誤。我們在前面的例子,我們用這一部分代碼將這個錯誤拋出到generator中:
if (err) {
// 向`*main()`中扔進一個錯誤
it.throw( err );
}
generator的yield
暫停特性不僅意味著我們可以從異步的函數(shù)調(diào)用那里得到看起來同步的return
值,還意味著我們可以同步地捕獲這些異步函數(shù)調(diào)用的錯誤!
那么我們看到了,我們可以將錯誤 拋入 generator,但是將錯誤 拋出 一個generator呢?和你期望的一樣:
function *main() {
var x = yield "Hello World";
yield x.toLowerCase(); // 引發(fā)一個異常!
}
var it = main();
it.next().value; // Hello World
try {
it.next( 42 );
}
catch (err) {
console.error( err ); // TypeError
}
當然,我們本可以用throw ..
手動地拋出一個錯誤,而不是制造一個異常。
我們甚至可以catch
我們throw(..)
進generator的同一個錯誤,實質(zhì)上給了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如何可以異步地迭代,這是一個用順序的可推理性來取代混亂如面條的回調(diào)的一個巨大進步。但我們丟掉了兩個非常重要的東西: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
),而且我們的 迭代器 控制代碼也不關(guān)心yield
的值。
但這里的Promise相關(guān)的foo(..)
在發(fā)起Ajax調(diào)用后返回一個promise。這暗示著我們可以用foo(..)
構(gòu)建一個promise,然后從generator中yield
出來,而后 迭代器 控制代碼將可以收到這個promise。
那么 迭代器 應(yīng)當對promise做什么?
它應(yīng)當監(jiān)聽promise的解析(完成或拒絕),然后要么使用完成消息繼續(xù)運行g(shù)enerator,要么使用拒絕理由向generator拋出錯誤。
讓我重復一遍,因為它如此重要。發(fā)揮Promise和generator的最大功效的自然方法是 yield
一個Promise,并將這個Promise連接到generator的 迭代器 的控制端。
讓我們試一下!首先,我們將Promise相關(guān)的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 );
}
}
在這個重構(gòu)中最強大的啟示是,*main()
內(nèi)部的代碼 更本就沒變! 在generator內(nèi)部,無論什么樣的值被yield
出去都是一個不可見的實現(xiàn)細節(jié),所以我們甚至不會察覺它發(fā)生了,也不用擔心它。
那么我們現(xiàn)在如何運行*main()
?我們還有一些管道的實現(xiàn)工作要做,接收并連接yield
的promise,使它能夠根據(jù)解析來繼續(xù)運行g(shù)enerator。我們從手動這么做開始:
var it = main();
var p = it.next().value;
// 等待`p` promise解析
p.then(
function(text){
it.next( text );
},
function(err){
it.throw( err );
}
);
其實,根本不費事,對吧?
這段代碼應(yīng)當看起來與我們早前做的很相似:手動地連接被錯誤優(yōu)先的回調(diào)控制的generator。與if (err) { it.throw..
不同的是,promise已經(jīng)為我們分割為完成(成功)與拒絕(失敗),否則 迭代器 控制是完全相同的。
現(xiàn)在,我們已經(jīng)掩蓋了一些重要的細節(jié)。
最重要的是,我們利用了這樣一個事實:我們知道*main()
里面只有一個Promise相關(guān)的步驟。如果我們想要能用Promise驅(qū)動一個generator而不管它有多少步驟呢?我們當然不想為每一個generator手動編寫一個不同的Promise鏈!要是有這樣一種方法該多好:可以重復(也就是“循環(huán)”)迭代的控制,而且每次一有Promise出來,就在繼續(xù)之前等待它的解析。
另外,如果generator在it.next()
調(diào)用期間拋出一個錯誤怎么辦?我們是該退出,還是應(yīng)該catch
它并把它送回去?相似地,要是我們it.throw(..)
一個Promise拒絕給generator,但是沒有被處理,又直接回來了呢?
帶有Promise的Generator運行器
你在這條路上探索得越遠,你就越能感到,“哇,要是有一些工具能幫我做這些就好了。”而且你絕對是對的。這是一種如此重要的模式,而且你不想把它弄錯(或者因為一遍又一遍地重復它而把自己累死),所以你最好的選擇是把賭注壓在一個工具上,而它以我們將要描述的方式使用這種特定設(shè)計的工具來 運行 yield
Promise的generator。
有幾種Promise抽象庫提供了這樣的工具,包括我的 asynquence 庫和它的runner(..)
,我們將在本書的在附錄A中討論它。
但看在學習和講解的份兒上,讓我們定義我們自己的名為run(..)
的獨立工具:
// 感謝Benjamin Gruenbaum (@benjamingr在GitHub)在此做出的巨大改進!
function run(gen) {
var args = [].slice.call( arguments, 1), it;
// 在當前的上下文環(huán)境中初始化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已經(jīng)完成運行了?
if (next.done) {
return next.value;
}
// 否則繼續(xù)執(zhí)行
else {
return Promise.resolve( next.value )
.then(
// 在成功的情況下繼續(xù)異步循環(huán),將解析的值送回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化的時間表上已經(jīng)出現(xiàn)了草案,對這個問題提供早期但強大的附加語法支持。顯然,現(xiàn)在還太早而不能保證其細節(jié),但是有相當大的可能性它將蛻變?yōu)轭愃朴谙旅娴臇|西:
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(..)
調(diào)用(意味著不需要工具庫!)來驅(qū)動和調(diào)用main()
——它僅僅像一個普通函數(shù)那樣被調(diào)用。另外,main()
不再作為一個generator函數(shù)聲明;它是一種新型的函數(shù):async function
。而最后,與yield
一個Promise相反,我們await
它解析。
如果你await
一個Promise,async function
會自動地知道做什么——它會暫停這個函數(shù)(就像使用generator那樣)直到Promise解析。我們沒有在這個代碼段中展示,但是調(diào)用一個像main()
這樣的異步函數(shù)將自動地返回一個promise,它會在函數(shù)完全完成時被解析。
提示: async
/ await
的語法應(yīng)該對擁有C#經(jīng)驗的讀者看起來非常熟悉,因為它們基本上是一樣的。
這個草案實質(zhì)上是為我們已經(jīng)衍生出的模式進行代碼化的支持,成為一種語法機制:用看似同步的流程控制代碼與Promise組合。將兩個世界的最好部分組合,來有效解決我們用回調(diào)遇到的幾乎所有主要問題。
這樣的ES7化草案已經(jīng)存在,并且有了早期的支持和熱忱的擁護。這一事實為這種異步模式在未來的重要性上信心滿滿地投了有力的一票。
Generator中的Promise并發(fā)
至此,所有我們展示過的是一種使用Promise+generator的單步異步流程。但是現(xiàn)實世界的代碼將總是有許多異步步驟。
如果你不小心,generator看似同步的風格也許會蒙蔽你,使你在如何構(gòu)造你的異步并發(fā)上感到自滿,導致性能次優(yōu)的模式。那么我們想花一點時間來探索一下其他選項。
想象一個場景,你需要從兩個不同的數(shù)據(jù)源取得數(shù)據(jù),然后將這些應(yīng)答組合來發(fā)起第三個請求,最后打印出最終的應(yīng)答。我們在第三章中用Promise探索過類似的場景,但這次讓我們在generator的環(huán)境下考慮它。
你的第一直覺可能是像這樣的東西:
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 );
這段代碼可以工作,但在我們特定的這個場景中,它不是最優(yōu)的。你能發(fā)現(xiàn)為什么嗎?
因為r1
和r2
請求可以——而且為了性能的原因,應(yīng)該——并發(fā)運行,但在這段代碼中它們將順序地運行;直到"http://some.url.1"
請求完成之前,"http://some.url.2"
URL不會被Ajax取得。這兩個請求是獨立的,所以性能更好的方式可能是讓它們同時運行。
但是使用generator和yield
,到底應(yīng)該怎么做?我們知道yield
在代碼中只是一個單獨的暫停點,所以你根本不能再同一時刻做兩次暫停。
最自然和有效的答案是基于Promise的異步流程,特別是因為它們的時間無關(guān)的狀態(tài)管理能力(參見第三章的“未來的值”)。
最簡單的方式:
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
是并發(fā)地(也就是“并行”)發(fā)起的Ajax請求promise。它們哪一個先完成都不要緊,因為promise會一直保持它們的解析狀態(tài)。
然后我們使用兩個連續(xù)的yield
語句等待并從promise中取得解析值(分別取到r1
和r2
中)。如果p1
首先解析,yield p1
會首先繼續(xù)執(zhí)行然后等待yield p2
繼續(xù)執(zhí)行。如果p2
首先解析,它將會耐心地保持解析值知道被請求,但是yield p1
將會首先停住,直到p1
解析。
不管是哪一種情況,p1
和p2
都將并發(fā)地運行,并且在r3 = yield request..
Ajax請求發(fā)起之前,都必須完成,無論以哪種順序。
如果這種流程控制處理模型聽起來很熟悉,那是因為它基本上和我們在第三章中介紹的,因Promise.all([ .. ])
工具成為可能的“門”模式是相同的。所以,我們也可以像這樣表達這種流程控制:
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解構(gòu)賦值來把var r1 = .. var r2 = ..
賦值簡寫為var [r1,r2] = results
。
換句話說,在generator+Promise的方式中,Promise所有的并發(fā)能力都是可用的。所以在任何地方,如果你需要比“這個然后那個”要復雜的順序異步流程步驟時,Promise都可能是最佳選擇。
Promises,隱藏起來
作為代碼風格的警告要說一句,要小心你在 你的generator內(nèi)部 包含了多少Promise邏輯。以我們描述過的方式在異步性上使用generator的全部意義,是要創(chuàng)建簡單,順序,看似同步的代碼,并盡可能多地將異步性細節(jié)隱藏在這些代碼之外。
比如,這可能是一種更干凈的方式:
// 注意:這是一個普通函數(shù),不是generator
function bar(url1,url2) {
return Promise.all( [
request( url1 ),
request( url2 )
] );
}
function *foo() {
// 將基于Promise的并發(fā)細節(jié)隱藏在`bar(..)`內(nèi)部
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()
內(nèi)部,它更干凈更清晰地表達了我們要做的事情:我們要求bar(..)
給我們一些results
,而我們將用yield
等待它的發(fā)生。我們不必關(guān)心在底層一個Promise.all([ .. ])
的Promise組合將被用來完成任務(wù)。
我們將異步性,特別是Promise,作為一種實現(xiàn)細節(jié)。
如果你要做一種精巧的序列流控制,那么將你的Promise邏輯隱藏在一個僅僅從你的generator中調(diào)用的函數(shù)里特別有用。舉個例子:
function bar() {
Promise.all( [
baz( .. )
.then( .. ),
Promise.race( [ .. ] )
] )
.then( .. )
}
有時候這種邏輯是必須的,而如果你直接把它扔在你的generator內(nèi)部,你就違背了大多數(shù)你使用generator的初衷。我們 應(yīng)當 有意地將這樣的細節(jié)從generator代碼中抽象出去,以使它們不會搞亂更高層的任務(wù)表達。
在創(chuàng)建功能強與性能好的代碼之上,你還應(yīng)當努力使代碼盡可能地容易推理和維護。
注意: 對于編程來說,抽象不總是一種健康的東西——許多時候它可能在得到簡潔的同時增加復雜性。但是在這種情況下,我相信你的generator+Promise異步代碼要比其他的選擇健康得多。雖然有所有這些建議,你仍然要注意你的特殊情況,并為你和你的團隊做出合適的決策。