你不知道JS:異步
第四章:生成器(Generators)
接上篇4-1
生成器委托(Generator Delegation)
在前一節(jié)中,我們展示了從生成器內(nèi)部調(diào)用普通函數(shù),以及為什么抽離實(shí)現(xiàn)細(xì)節(jié)是個(gè)有用的技術(shù)(像異步Promise流)。但采用普通函數(shù)的主要缺點(diǎn)是必須遵循不同函數(shù)規(guī)則,這意味著無法像生成器一樣使用yield來暫停函數(shù)自身。
你突然想到,通過輔助函數(shù)run(..),可以試著從另一個(gè)生成器中調(diào)用生成器,形如:
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" );
// "delegating" to `*foo()` via `run(..)`
var r3 = yield run( foo );
console.log( r3 );
}
run( bar );
通過使用run(..) utility,我們在*bar()內(nèi)部運(yùn)行*foo()。此處,我們利用了這一事實(shí),即早先定義的run(..)返回一個(gè)promise,當(dāng)該生成器運(yùn)行直至結(jié)束(或者發(fā)生錯(cuò)誤)時(shí),該promise得到解析。因此,如果我們yield出另一個(gè)run(..)調(diào)用生成的promise給run(..)實(shí)例,它會自動暫停*bar()直至*foo()完成。
但是有個(gè)更好的方法來整合*bar()內(nèi)的*foo()調(diào)用,稱為yield委托。yield委托的特殊語法是:yield * _(注意多出的*)。在我們看它如何在之前例子中工作之前,先看一個(gè)簡單點(diǎn)的場景:
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
注意: 類似于本章早些時(shí)候的注意事項(xiàng),即為什么我偏愛function *foo() ..而不是function* foo() ..,同樣,我也偏愛--不同于其它大多數(shù)相關(guān)文檔--用yield *foo(),而不是yield* foo()。*的位置純粹是風(fēng)格上的喜好,由你自己決定。但我覺得風(fēng)格一致比較有吸引力。
yield *foo()委托是如何工作的呢?
首先,foo()調(diào)用創(chuàng)建了一個(gè)迭代器。之后,yield *委托/轉(zhuǎn)移迭代器實(shí)例的控制權(quán)(當(dāng)前*bar()生成器的)給另一個(gè)*foo()迭代器。
因此,前兩個(gè)it.next()調(diào)用控制*bar(),但是當(dāng)進(jìn)行第三個(gè)it.next()調(diào)用時(shí),*foo()啟動了,現(xiàn)在我們控制foo()而不是*bar()了。這就是稱為委托的原因--*bar()將自己迭代的控制權(quán)委托給*foo()。
一旦it迭代器控制迭代完整個(gè)*foo()迭代器,就會自動返回到*bar()控制之中。
現(xiàn)在回到之前的三個(gè)序列化Ajax請求的例子:
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" );
// "delegating" to `*foo()` via `yield*`
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
和早先版本唯一的不同是采用了yield *foo(),而不是之前的yield run(foo)
注意: yield * yield出迭代控制權(quán),而不是生成器的控制權(quán);當(dāng)你激活*foo()生成器時(shí),yield委托到它的迭代器。但實(shí)際上也可以yield委托任何iterable,yield *[1,2,3]會處理[1,2,3]的默認(rèn)迭代器。
為什么委托?(Why Delegation?)
yield委托的主要目的是組織代碼,那樣的話就和普通的函數(shù)調(diào)用沒什么區(qū)別了。
假設(shè)兩個(gè)模塊分別提供了foo()和bar()方法,bar()調(diào)用foo()。
分開的原因通常是為合理的代碼組織考慮,即可能在不同的函數(shù)中調(diào)用它們。比如,可能有些時(shí)候,foo()是單獨(dú)調(diào)用的,有時(shí)是bar()調(diào)用foo()。
幾乎基于同樣的原因,即保持生成器分離有助于提高程序的可讀性、可維護(hù)性和可調(diào)試性。從那個(gè)角度講,當(dāng)在*bar()內(nèi)部時(shí),yield *是手動迭代*foo()步驟的簡寫形式。
如果*foo()的步驟是異步的,手動方法可能特別復(fù)雜,這就是為什么需要run(..)utility。如上所示,yield *foo()就不需要run(..)utility的子實(shí)例(比如run(foo))了。
委托信息(Delegating Messages)
你可能想知道yield委托是如何實(shí)現(xiàn)迭代器控制和兩路信息傳遞的。通過yield委托,仔細(xì)觀察信息的流入、流出:
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`-delegation!
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
特別關(guān)注一下it.next(3)調(diào)用后的處理步驟:
- 值
3被傳入*foo()內(nèi)(通過*bar()內(nèi)的yield委托)等待的yield "C"表達(dá)式。 - 之后
*foo()調(diào)用return "D",但這個(gè)值并沒有返回給外面的it.next(3)。 - 反而,
D值返回作為*bar()內(nèi)等待的yield *foo()表達(dá)式的結(jié)果--當(dāng)*foo()被窮盡時(shí),這種yield委托表達(dá)本質(zhì)上已經(jīng)被暫停了。因此*bar()內(nèi)的"D"最終被打印出來了。 -
yield "E"在*bar()內(nèi)部被調(diào)用,E值被yield到外部,作為it.next(3)調(diào)用的結(jié)果。
從外部迭代器(it)的角度來看,控制初始生成器和委托生成器似乎沒什么區(qū)別。
事實(shí)上,yield委托甚至沒有必要定向到另一個(gè)生成器,可以只定向到一個(gè)非生成器、通用iterable。比如:
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-delegation to a non-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
注意下這個(gè)例子和前一個(gè)例子中信息的接收和報(bào)告的區(qū)別。
最不可思議的是,array的默認(rèn)迭代器不關(guān)心通過next(..)調(diào)用傳入的任何信息,因此值2,3和4會被忽略。另外,因?yàn)槟莻€(gè)迭代器沒有顯式的return值(不像之前的*foo()),當(dāng)結(jié)束的時(shí)候,yield *表達(dá)式獲得一個(gè)undefined。
也委托異常?。‥xceptions Delegated, Too!)
與yield委托兩路透明傳值一樣,錯(cuò)誤/異常也是兩路傳值的:
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
這段代碼中有些東西需要注意:
- 當(dāng)調(diào)用
it.throw(2)時(shí),會將錯(cuò)誤信息2發(fā)送給*bar(),*bar()會將其委托給*foo(),之后*foo()catch它并處理。之后yield "C"將C返回作為it.throw(2)調(diào)用的返回value。 -
*foo()內(nèi)部下一個(gè)throw拋出的"D"值傳播到*bar()中,*bar()catch它并處理。之后yield "E"返回E作為it.next(3)調(diào)用的返回value。 - 之后,
*baz()throw出的異常沒有在*bar()中捕獲--盡管我們確實(shí)在外面catch它--因此,*baz()和*bar()都被設(shè)為完成狀態(tài)。這段代碼之后,你可能無法利用隨后的next(..)調(diào)用獲得"G"值--它們只會簡單地返回undefined作為value。
委托異步(Delegating Asynchrony)
讓我們回到早先的多個(gè)序列化Ajax請求的yield委托例子:
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()內(nèi)部調(diào)用yield *foo(),而不是yield run(foo)。
在這一例子的前一版本中,Promise機(jī)制(由run(..)控制)用來傳遞*foo()內(nèi)return r3的值給*bar()內(nèi)的局部變量r3?,F(xiàn)在,現(xiàn)在,那個(gè)值通過yield *機(jī)制直接返回。
除此之外,行為完全一致。
委托“遞歸”(Delegating "Recursion")
當(dāng)然,yield委托能夠跟蹤盡可能多的委托步驟。你甚至可以對異步的生成器“遞歸”--生成器yield委托給自己--使用yield委托:
function *foo(val) {
if (val > 1) {
// generator recursion
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(..) utility本該以run(foo,3)的形式調(diào)用,因?yàn)樗С指郊訁?shù),用來傳遞給生成器的初始化過程。然而,這里我們用了沒有參數(shù)的*bar(),來突出yield *的靈活性。
那段代碼遵循什么執(zhí)行步驟?堅(jiān)持住,細(xì)節(jié)描述有點(diǎn)復(fù)雜:
-
run(bar)啟動*bar()生成器。 -
foo(3)創(chuàng)建一個(gè)*foo(3)的迭代器,傳入3作為val的值。 - 因?yàn)?code>3>1,
foo(2)創(chuàng)建了另一個(gè)迭代器,傳入2作為val的值。 - 因?yàn)?code>2>1,
foo(1)創(chuàng)建了另一個(gè)迭代器,傳入1作為val的值。 -
1>1是false,因此之后以值1調(diào)用request(..),獲得第一個(gè)Ajax調(diào)用返回的promise。 - 那個(gè)promise被
yield出來,返回到*foo(2)生成器實(shí)例。 -
yield *將那個(gè)promise傳回到*foo(3)生成器實(shí)例。另一個(gè)yield *將promise傳出給*bar()生成器實(shí)例。再一次,另一個(gè)yield *將promise傳出給run()utility,它會等待那個(gè)promise(第一個(gè)Ajax請求)解析。 - 當(dāng)promise解析后,它的fulfillment信息被發(fā)送出去用來恢復(fù)
*bar(),信息通過yield *傳入到*foo(3)實(shí)例,之后通過yield *傳入到*foo(2)實(shí)例,之后通過yield *傳入到*foo(3)中等待的普通yield中。 - 現(xiàn)在,第一個(gè)調(diào)用的Ajax響應(yīng)立即從
*foo(3)生成器實(shí)例中return出來,之后將其返回作為*foo(2)實(shí)例中yield *表達(dá)式的結(jié)果,并將其賦給局部變量val。 - 在
*foo(2)內(nèi)部,request(..)發(fā)起了第二個(gè)Ajax請求,它的promise被yield回*foo(1)實(shí)例,之后yield *將其一路傳到run(..)(重復(fù)第7步)。當(dāng)promise解析后,第二個(gè)Ajax響應(yīng)一路傳回*foo(2)生成器實(shí)例,賦給局部變量val。 - 最后,
request(..)發(fā)起了第三個(gè)Ajax請求,它的promise返回給run(..),之后它的解析值一路返回,直至被return,以便回到*bar()中等待的yield *表達(dá)式。
唷!精神飽受摧殘了?你可能想再讀幾次,之后吃點(diǎn)零食清空下大腦!
生成器并發(fā)(Generator Concurrency)
如第一章和本章早些時(shí)候提到的,兩個(gè)同時(shí)運(yùn)行的“進(jìn)程”可以協(xié)作式的交叉各自的操作,很多時(shí)候能夠yield出強(qiáng)大的異步表達(dá)式。
坦白來講,早先的多個(gè)生成器并發(fā)交叉的例子證明了是多么的令人感到混亂。但我們暗示了某些場合,這種能力非常有用。
回想下第一章中的場景,兩個(gè)不同的同時(shí)Ajax響應(yīng)處理函數(shù)需要相互協(xié)調(diào),以便數(shù)據(jù)通信不會造成競態(tài)。我們把響應(yīng)像這樣放入到res數(shù)組中:
function response(data) {
if (data.url == "http://some.url.1") {
res[0] = data;
}
else if (data.url == "http://some.url.2") {
res[1] = data;
}
}
但在這一場景中,我們?nèi)绾尾l(fā)使用多個(gè)生成器呢?
// `request(..)` is a Promise-aware Ajax utility
var res = [];
function *reqData(url) {
res.push(
yield request( url )
);
}
注意: 此處我們打算使用*reqData(..)的兩個(gè)生成器實(shí)例,但是和運(yùn)行兩個(gè)不同生成器的單個(gè)實(shí)例沒有什么區(qū)別;兩種方法的思考方式是一致的。一會我們看看兩個(gè)不同生成器的協(xié)調(diào)。
我們會使用協(xié)調(diào)排序,以便res.push(..)能夠?qū)⒅狄钥深A(yù)料的順序放置
,而不是手動地分為res[0]和res[1]賦值。表達(dá)邏輯因此也可以更清晰些。
但實(shí)際上該如何編排這種交互呢?首先,讓我們手動用Promise實(shí)現(xiàn)一下:
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(..)的兩個(gè)實(shí)例都開始發(fā)起Ajax請求,之后通過yield暫停。之后當(dāng)p1解析后,我們選擇恢復(fù)第一個(gè)實(shí)例,之后p2的解析結(jié)果會重啟第二個(gè)實(shí)例。這樣的話,我們用promise編排來確保res[0]存放第一個(gè)響應(yīng),res[1]存放第二個(gè)響應(yīng)。
但坦白來講,這種方式太手動化了,并沒有真正讓生成器編排它們,這才是強(qiáng)大之處(譯者注:指讓生成器編排)。讓我們換個(gè)方式試一下:
// `request(..)` is a Promise-aware Ajax utility
var res = [];
function *reqData(url) {
var data = yield request( url );
// transfer control
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();
} );
OK,好一點(diǎn)了(盡管仍然是手動的?。?,因?yàn)楝F(xiàn)在*reqData(..)的兩個(gè)實(shí)例真的并發(fā)、獨(dú)立(至少從第一部分來講)運(yùn)行。
在上一個(gè)代碼中,直到第一個(gè)實(shí)例完全結(jié)束之后,第二個(gè)才給出它的數(shù)據(jù)。但這里,只要各自的響應(yīng)返回,兩個(gè)實(shí)例都能盡快的接收到數(shù)據(jù),之后每個(gè)實(shí)例為了控制轉(zhuǎn)移目的,作了另一個(gè)yield。之后通過在Promise.all([ .. ])的處理函數(shù)中來選擇恢復(fù)順序。
不大明顯的一點(diǎn)是,由于對稱性,這個(gè)方法暗含了一種更簡單的復(fù)用utility形式。我們可以做的更好。假設(shè)使用一個(gè)稱為runAll(..)的utility:
// `request(..)` is a Promise-aware Ajax utility
var res = [];
runAll(
function*(){
var p1 = request( "http://some.url.1" );
// transfer control
yield;
res.push( yield p1 );
},
function*(){
var p2 = request( "http://some.url.2" );
// transfer control
yield;
res.push( yield p2 );
}
);
注意: 我們沒有展示出runAll(..)的實(shí)現(xiàn)代碼,不僅因?yàn)樘L,而且還是早先實(shí)現(xiàn)的run(..)邏輯的拓展。因此,作為讀者很好的練習(xí)補(bǔ)充,試著從run(..)演化代碼,使其運(yùn)行原理和想象的runAll(..)一樣。另外,我的asynquence庫提供了之前提到了runner(..)utility,其內(nèi)建有這種功能,會在本書的附錄A中討論。
以下是runAll(..)內(nèi)部的運(yùn)行方式:
- 第一個(gè)生成器獲得來自
"http://some.url.1"的第一個(gè)Ajax響應(yīng)的promise,之后yield控制權(quán)回到runAll(..)utility。 - 第二個(gè)生成器運(yùn)行,同樣處理
"http://some.url.2",yield控制權(quán)回到runAll(..)utility。 - 第一個(gè)生成器恢復(fù),之后
yield出它的promisep1,這種情況下,runAll(..)utility和之前的run(..)做的一樣,在其內(nèi)部,等待promise的解析,之后恢復(fù)同一個(gè)生成器(不是控制權(quán)轉(zhuǎn)移!)。當(dāng)p1解析后,runAll(..)用解析值再次恢復(fù)第一個(gè)生成器,之后res[0]就被賦了該值。當(dāng)?shù)谝粋€(gè)生成器結(jié)束之后,有個(gè)隱式的控制權(quán)轉(zhuǎn)移。 - 第二個(gè)生成器恢復(fù),
yield出promisep2,等待其解析。一旦解析,runAll(..)以解析值恢復(fù)第二個(gè)生成器,并且設(shè)置res[1]。
在這個(gè)運(yùn)行例子中,我們使用了外部變量res來保存兩個(gè)不同Ajax響應(yīng)的結(jié)果--這使得并發(fā)協(xié)調(diào)成為可能。
但進(jìn)一步擴(kuò)展下runAll(..),提供一個(gè)由多個(gè)生成器實(shí)例共享的內(nèi)部變量空間可能更好,比如下面我們稱為data的空對象。另外,也可以yield出非Promise變量,并把它們傳遞給下一個(gè)生成器。
考慮如下:
// `request(..)` is a Promise-aware Ajax utility
runAll(
function*(data){
data.res = [];
// transfer control (and message pass)
var url1 = yield "http://some.url.2";
var p1 = request( url1 ); // "http://some.url.1"
// transfer control
yield;
data.res.push( yield p1 );
},
function*(data){
// transfer control (and message pass)
var url2 = yield "http://some.url.1";
var p2 = request( url2 ); // "http://some.url.2"
// transfer control
yield;
data.res.push( yield p2 );
}
);
這種形式中,兩個(gè)生成器不僅協(xié)調(diào)控制權(quán)轉(zhuǎn)移,而且還相互通信,都是通過data.res和交換rul1和rul2值yield出的信息。相當(dāng)強(qiáng)大!
這樣的實(shí)現(xiàn)也為一種更復(fù)雜的稱為CSP(通信序列進(jìn)程,Communicating Sequential Processes)的異步技術(shù)充當(dāng)了概念基礎(chǔ),我們會在本書的附錄B中討論。
Thunks
迄今為止,我們一直假定生成器yield出Promise--通過如run(..)的輔助utility讓Promise恢復(fù)生成器的運(yùn)行--是用生成器管理異步的最好的方法。說明白點(diǎn),它就是。
但我們跳過了另一種被廣泛接受的模式,因此,為了保證完整性,我們簡單看下。
在一般的計(jì)算機(jī)科學(xué)中,有一個(gè)很老的、在JS之前的概念,叫"thunk"。就不提它的歷史了,在JS中,thunk簡單點(diǎn)的表達(dá)就是一個(gè)調(diào)用另一個(gè)函數(shù)的函數(shù)(沒有任何參數(shù))。
換句話說,就是用函數(shù)定義包裝一個(gè)函數(shù)調(diào)用--用它所需的任何參數(shù)--來推遲調(diào)用的執(zhí)行,包裝函數(shù)就稱為一個(gè)thunk。當(dāng)之后執(zhí)行thunk時(shí),最終會調(diào)用初始的函數(shù)。
例如:
function foo(x,y) {
return x + y;
}
function fooThunk() {
return foo( 3, 4 );
}
// later
console.log( fooThunk() ); // 7
因此,同步的thunk相當(dāng)直接。但要是異步的thunk呢?我們可以簡單地?cái)U(kuò)展thunk定義,允許接收一個(gè)回調(diào)。
考慮如下:
function foo(x,y,cb) {
setTimeout( function(){
cb( x + y );
}, 1000 );
}
function fooThunk(cb) {
foo( 3, 4, cb );
}
// later
fooThunk( function(sum){
console.log( sum ); // 7
} );
如你所見,fooThunk(..)只期望一個(gè)cb(..)參數(shù),因?yàn)樗呀?jīng)預(yù)設(shè)有值3和4(分別對應(yīng)x和y),并且準(zhǔn)備傳給foo(..)。thunk只需耐心等待做最后一件事:回調(diào)。
然而,你并不想手動實(shí)現(xiàn)thunk。因此,讓我們實(shí)現(xiàn)一種utility來為我們做這種包裝工作。
考慮如下:
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 );
// later
fooThunk( function(sum) {
console.log( sum ); // 7
} );
提示: 此處我們假定初始函數(shù)(foo(..))希望回調(diào)處在最后的位置,其余任何參數(shù)都是在它之前。這是異步JS函數(shù)標(biāo)準(zhǔn)中相當(dāng)常見的“標(biāo)準(zhǔn)”??梢苑Q之為“callback-last style”,如果處于某種原因,你需要處理”callback-first style“的形式,只需在utility中使用args.unshift(..),而不是args.push(..)。
之前的thunkify(..)實(shí)現(xiàn)采用foo(..)函數(shù)引用和其它任何需要的參數(shù),之后返回thunk本身(fooThunk(..))。然而,這并不是JS中典型的thunk方法。
如果不是太困惑的話,thunkify(..)utility會生成一個(gè)函數(shù),該函數(shù)能生成thunks,而不是thunkify(..)直接生成thunks。
哦。。。耶。
考慮如下:
function thunkify(fn) {
return function() {
var args = [].slice.call( arguments );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
};
}
主要區(qū)別在于額外的return function() { .. }層,以下是用法的不同:
var whatIsThis = thunkify( foo );
var fooThunk = whatIsThis( 3, 4 );
// later
fooThunk( function(sum) {
console.log( sum ); // 7
} );
很明顯,這段代碼暗含的大問題是whatIsThis如何稱呼。它不是thunk,它會生成thunk。有點(diǎn)像"thunk"“工廠”,似乎對其命名沒有個(gè)統(tǒng)一的標(biāo)準(zhǔn)。
因此,我的提議是“thunkory”(“thunk”+“factory”)。那么,thunkify(..)生成一個(gè)thunkory,之后thunkory生成thunks。原因和我第三章提議的promisory差不多:
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// later
fooThunk1( function(sum) {
console.log( sum ); // 7
} );
fooThunk2( function(sum) {
console.log( sum ); // 11
} );
注意: 運(yùn)行的foo(..)期望的回調(diào)形式不是“error-first style”。當(dāng)然,“error-first style”更常見。如果foo(..)預(yù)期會有某種合理的錯(cuò)誤生成,我們可以修改一下,采用錯(cuò)誤優(yōu)先回調(diào)。隨后的thunkify(..)不關(guān)心假定的是哪種形式的回調(diào)。使用方法上的唯一區(qū)別就是fooThunk1(function(err,sum){..。
暴露thunkory方法--而不是早先的thunkify(..)將中間步驟隱藏起來--似乎增加了不必要的復(fù)雜度。但通常而言,在程序開始之前,生成thunkory來包裝現(xiàn)有的API方法很有用,當(dāng)需要thunk的時(shí)候,可以傳入并調(diào)用這些thunkory。兩個(gè)分開的步驟實(shí)現(xiàn)了更清晰的功能分離。
舉例如下:
// cleaner:
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// instead of:
var fooThunk1 = thunkify( foo, 3, 4 );
var fooThunk2 = thunkify( foo, 5, 6 );
不管你喜歡顯式還是隱式地處理thunkory,thunk fooThunk1(..)和fooThunk2(..)的用法仍然一樣。
s/promise/thunk/
那么,thunk如何處理生成器呢?
通常將thunk比作promise:它們不是直接可以相互取代的,因?yàn)樵谛袨樯喜⒉粚Φ取O啾扔诼惚嫉膖hunk,Promise功能更強(qiáng),更值得信任。
但從另一方面來說,它們都可以視作請求一個(gè)值,并且都是異步的。
回想下第三章中我們定義的用來promisify函數(shù)的utility,稱為Promise.wrap(..)--我們也可稱之為promisify(..)!這個(gè)Promise 包裝utility并不生成Promise;它生成promisory,promisory能夠生成Promise。這與討論的thunkory和thunk完全一致。
為了說明一致性,首先將早先的foo(..)例子改為“error-first style”回調(diào):
function foo(x,y,cb) {
setTimeout( function(){
// assume `cb(..)` as "error-first style"
cb( null, x + y );
}, 1000 );
}
現(xiàn)在,我們比較下使用thunkify(..)和promisify(..)(即第三章中的Promise.wrap(..)):
// symmetrical: constructing the question asker
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );
// symmetrical: asking the question
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );
// get the thunk answer
fooThunk( function(err,sum){
if (err) {
console.error( err );
}
else {
console.log( sum ); // 7
}
} );
// get the promise answer
fooPromise
.then(
function(sum){
console.log( sum ); // 7
},
function(err){
console.error( err );
}
);
本質(zhì)而言,thunkory和promisory都在問一個(gè)問題(請求值),thunk fooThunk和promise fooPromise分別代表問題的未來答案。從那個(gè)角度而言,一致性很明顯。
有這樣的想法之后,為了實(shí)現(xiàn)異步,yield Promise的生成器也可以yield thunk。我們所需的只是個(gè)更精簡的run(..) utility(和之前的差不多),不僅能夠搜尋并連接到一個(gè)yield出的Promise,而且能夠?yàn)?code>yield出的thunk提供回調(diào)函數(shù)。
考慮如下:
function *foo() {
var val = yield request( "http://some.url.1" );
console.log( val );
}
run( foo );
在這個(gè)例子中,request(..)既可以是個(gè)返回promise的promisory,又可以是個(gè)返回thunk的thunkory。從生成器內(nèi)部代碼邏輯角度而言,我們不關(guān)心實(shí)現(xiàn)細(xì)節(jié),這相當(dāng)強(qiáng)大!
因此,request(..)可以是這樣:
// promisory `request(..)` (see Chapter 3)
var request = Promise.wrap( ajax );
// vs.
// thunkory `request(..)`
var request = thunkify( ajax );
最后,作為早先run(..)utility的thunk式補(bǔ)丁,可能需要如下的邏輯:
// ..
// did we receive a thunk back?
else if (typeof next.value == "function") {
return new Promise( function(resolve,reject){
// call the thunk with an error-first callback
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 );
}
);
}
現(xiàn)在,我們的生成器既可以調(diào)用promisory yield Promise,也可以調(diào)用thunkory yield thunk,并且無論哪一種情形,run(..)都能夠處理那個(gè)值并且使用它來等待其完成,繼而恢復(fù)生成器。
由于對稱性,這兩個(gè)方法看起來一致。然而,我們應(yīng)該指出的是,只有從Promise或者thunk代表能夠推進(jìn)生成器執(zhí)行的未來值的角度來說,這才是正確的。
從更大角度而言,thunk內(nèi)部幾乎沒有任何Promise所具備的可信任性和可組合性保證。在這種特定的生成器異步模式中,使用thunk作為Promise的替身是有效地,但相比于Promise提供的種種好處(見第三章),使用thunk應(yīng)視為不太理想的方案。
如果可以選擇,優(yōu)先用yield pr而不是yield th。但讓run(..)utility可以處理這兩種值類型沒什么問題。
提示: 我的asynquence庫中的runner(..) utility,能夠處理Promise,thunk和asynquence序列。
ES6前的生成器(Pre-ES6 Generators)
現(xiàn)在,你很希望相信生成器是異步編程工具箱中一個(gè)非常重要的添加項(xiàng)。但它是ES6中新增的語法,意味著你無法像Promise(只是一個(gè)新的API)那樣polyfill 生成器。那么如果無法忽略ES6前的瀏覽器,我們?nèi)绾螌⑸善饕霝g覽器中呢?
對于所有ES6中的語法擴(kuò)展,有些工具--最常用的術(shù)語叫轉(zhuǎn)譯器,全稱轉(zhuǎn)換-編譯--可以提供給你ES6語法,并將其轉(zhuǎn)換為對等的(但相當(dāng)丑陋)ES6前的語法。因此,生成器可以轉(zhuǎn)譯為具有同樣的行為的代碼,能夠在ES5或者更低的版本JS中運(yùn)行。
但怎么轉(zhuǎn)呢?yield“魔法”聽起來很明顯不容易轉(zhuǎn)譯。其實(shí)在早先的基于閉包的迭代器中,我們已經(jīng)暗示了一種解決方案。
手動轉(zhuǎn)換(Manual Transformation)
在我們討論轉(zhuǎn)譯器之前,讓我們研究一下手動轉(zhuǎn)譯生成器是如何工作的。這不僅僅是個(gè)學(xué)術(shù)活動,也可以幫助你增強(qiáng)對其工作原理的理解。
考慮如下:
// `request(..)` is a Promise-aware Ajax utility
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" );
第一點(diǎn)需要注意的是,我們?nèi)匀恍枰粋€(gè)能被調(diào)用的普通foo()函數(shù),并且仍然需要返回一個(gè)迭代器。因此,簡單勾畫一下非生成器轉(zhuǎn)譯:
function foo(url) {
// ..
// make and return an iterator
return {
next: function(v) {
// ..
},
throw: function(e) {
// ..
}
};
}
var it = foo( "http://some.url.1" );
接下來要關(guān)心的是生成器通過暫停它的域/狀態(tài)來實(shí)現(xiàn)其“魔法”,但我們可以用函數(shù)閉包來模擬。為理解如何寫這些代碼,我們首先用狀態(tài)值注釋一下生成器的不同部分:
// `request(..)` is a Promise-aware Ajax utility
function *foo(url) {
// STATE *1*
try {
console.log( "requesting:", url );
var TMP1 = request( url );
// STATE *2*
var val = yield TMP1;
console.log( val );
}
catch (err) {
// STATE *3*
console.log( "Oops:", err );
return false;
}
}
注意: 為了更準(zhǔn)確地說明,我們采用臨時(shí)變量TMP1,將val = yield request..語句分成兩部分。request(..)在狀態(tài)*1*時(shí)發(fā)生,將其完成值賦給變量val在狀態(tài)*2*時(shí)發(fā)生。當(dāng)將代碼轉(zhuǎn)換為它的非生成器對等時(shí),我們需要去掉中間的TMP1。
換句話說,*1*是開始狀態(tài),*2*是request(..)成功狀態(tài),*3*是request(..)失敗狀態(tài)。你可以想象一下附加的yield步驟是如何編碼成附加的狀態(tài)。
回到我們轉(zhuǎn)譯的生成器,讓我們在閉包中定義一個(gè)變量state,用來追蹤狀態(tài):
function foo(url) {
// manage generator state
var state;
// ..
}
現(xiàn)在,在處理狀態(tài)的閉包中定義一個(gè)稱為process(..)的函數(shù),采用switch語句:
// `request(..)` is a Promise-aware Ajax utility
function foo(url) {
// manage generator state
var state;
// generator-wide variable declarations
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;
}
}
// ..
}
生成器中的每個(gè)狀態(tài)對應(yīng)switch語句中的case。每次需要處理新狀態(tài)時(shí),就要調(diào)用process(..)。之后我們會講下它的工作原理。
對于任何一般的生成器變量申明(val),我們將其移至process(..)外的var聲明中,這樣可供多次process(..)調(diào)用使用。但是“塊域”變量err僅狀態(tài)*3*需要使用,因此我們將其放在塊里。
在狀態(tài)*1*時(shí),我們作了return request(..),而不是yield request(..)。在終止?fàn)顟B(tài)*2*中,沒有顯式的需要return,所以只有個(gè)簡單的return;這和return undefined一樣。在終止?fàn)顟B(tài)*3*中,有個(gè)return false,我們能夠保存該值。
現(xiàn)在我們需要在迭代器內(nèi)部定義代碼,以便能夠合適地調(diào)用process(..):
function foo(url) {
// manage generator state
var state;
// generator-wide variable declarations
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;
}
}
// make and return an iterator
return {
next: function(v) {
// initial state
if (!state) {
state = 1;
return {
done: false,
value: process()
};
}
// yield resumed successfully
else if (state == 1) {
state = 2;
return {
done: true,
value: process( v )
};
}
// generator already completed
else {
return {
done: true,
value: undefined
};
}
},
"throw": function(e) {
// the only explicit error handling is in
// state *1*
if (state == 1) {
state = 3;
return {
done: true,
value: process( e )
};
}
// otherwise, an error won't be handled,
// so just throw it right back out
else {
throw e;
}
}
};
}
這段代碼是如何工作的呢?
-
對迭代器
next()的第一次調(diào)用會將生成器從未初始狀態(tài)轉(zhuǎn)到狀態(tài)1,之后調(diào)用process()來處理該狀態(tài)。request()的返回值,即Ajax的響應(yīng)promise,被返回作為next()調(diào)用的value屬性值。 - 如果Ajax請求成功,第二個(gè)
next(..)調(diào)用需要傳入Ajax響應(yīng)值,會將狀態(tài)切換為2.process(..)被再次調(diào)用(這次需要傳入Ajax響應(yīng)值),從next(..)返回的value屬性就為undefined。 - 然而,如果Ajax請求失敗,應(yīng)當(dāng)以error調(diào)用
throw(..),會將狀態(tài)從1變成3(而不是2)。process(..)再次被調(diào)用,這次是以錯(cuò)誤值。那個(gè)case返回false,會被設(shè)為throw(..)調(diào)用返回的value屬性值。
從外部看--它只和迭代器交互--這個(gè)foo(..)普通函數(shù)和*foo(..)表現(xiàn)的完全一樣。因此,我們已經(jīng)有效地將ES6生成器轉(zhuǎn)譯為pre-ES6的兼容代碼!
之后,我們可以手動實(shí)例化生成器并控制其迭代器--調(diào)用var it = foo("..")和it.next(..),諸如此類--或者更好的方法,我們可以把它傳給之前定義的run(..)utility,即run(foo,"..")。
自動轉(zhuǎn)譯(Automatic Transpilation)
之前的手動轉(zhuǎn)譯ES6生成器為pre-ES6等價(jià)代碼練習(xí)從概念上教會了我們生成器是如何工作的。但那種轉(zhuǎn)譯真的很復(fù)雜,并且不好移植到其它生成器。手動實(shí)現(xiàn)相當(dāng)不切實(shí)際,會完全消除生成器的好處。
但幸運(yùn)的是,已經(jīng)有幾個(gè)工具庫能夠?qū)S6生成器轉(zhuǎn)譯成類似我們之前轉(zhuǎn)譯的結(jié)果。它們不僅為我們做了繁重的工作,而且也處理了我們一帶而過的幾個(gè)復(fù)雜問題。
其中一個(gè)工具是regenerator(https://facebook.github.io/regenerator/),來自Facebook的小folk。
如果我們使用regenerator轉(zhuǎn)譯之前的生成器,以下是轉(zhuǎn)譯后的代碼:
// `request(..)` is a Promise-aware Ajax utility
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。
當(dāng)然,有個(gè)折中,即regenerator轉(zhuǎn)譯需要一個(gè)輔助庫regeneratorRuntime,其中包含了管理通用生成器/迭代器的所有復(fù)用邏輯。許多那樣的樣版代碼看起來不同于我們的版本,但即使是那樣,也能看到相關(guān)的概念,比如用來追蹤生成器狀態(tài)的context$1$0.next = 4。
主要的挑戰(zhàn)是,生成器不僅僅限于ES6+環(huán)境中。一旦你理解了概念,就可以在代碼中使用它們,采用工具來將其轉(zhuǎn)譯成舊環(huán)境兼容的代碼。
相比pre-ES6 Promise,只需用個(gè)Promise API polyfill,這里的工作量顯然更多,但努力是完全值得的,因?yàn)樯善髂軌蛞院侠?、有意義、看起來同步、序列化的方式更好地表達(dá)異步流控制。
一旦你迷上了生成器,你就再也不想回到異步的意大利面條式的回調(diào)地獄了!
回顧(Review)
生成器是一種新的ES6函數(shù)類型,它不是像普通函數(shù)那樣運(yùn)行直至結(jié)束的。而是,生成器可以在中間過程(完全保持自身狀態(tài))暫停,并且之后可以從暫停的地方恢復(fù)。
這種暫停/恢復(fù)的切換是協(xié)作式的,而不是搶占式的。這意味著生成器有獨(dú)有的能力暫停自己(采用yield關(guān)鍵字),之后控制生成器的迭代器能夠恢復(fù)生成器(通過next(..))。
yield/next()對不僅僅是一種控制機(jī)制,實(shí)際上還是一種兩路信息傳遞機(jī)制。本質(zhì)上,yield..表達(dá)式暫停生成器并等待值,下一個(gè)next(..)調(diào)用傳回一個(gè)值(或者隱式的undefined)來恢復(fù)生成器。
生成器關(guān)于異步流控制的關(guān)鍵優(yōu)點(diǎn)是,生成器內(nèi)部的代碼能夠以同步/序列化的方式表示任務(wù)序列。技巧在于我們將異步隱藏到yield關(guān)鍵字之后了--將異步移到生成器的迭代器控制的代碼部分。
換句話說,生成器實(shí)現(xiàn)了異步代碼的序列化、同步化和阻塞性,這可以讓我們的大腦能夠更自然地推演代碼,解決基于回調(diào)的異步的兩大缺點(diǎn)之一。