特別說明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
Generator中的Promise并發(fā)
至此,所有我們展示過的是一種使用Promise+generator的單步異步流程。但是現(xiàn)實世界的代碼將總是有許多異步步驟。
如果你不小心,generator看似同步的風(fēng)格也許會蒙蔽你,使你在如何構(gòu)造你的異步并發(fā)上感到自滿,導(dǎo)致性能次優(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ā)能力都是可用的。所以在任何地方,如果你需要比“這個然后那個”要復(fù)雜的順序異步流程步驟時,Promise都可能是最佳選擇。
Promises,隱藏起來
作為代碼風(fēng)格的警告要說一句,要小心你在 你的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() {
return Promise.all( [
baz( .. )
.then( .. ),
Promise.race( [ .. ] )
] )
.then( .. )
}
有時候這種邏輯是必須的,而如果你直接把它扔在你的generator內(nèi)部,你就違背了大多數(shù)你使用generator的初衷。我們 應(yīng)當(dāng) 有意地將這樣的細節(jié)從generator代碼中抽象出去,以使它們不會搞亂更高層的任務(wù)表達。
在創(chuàng)建功能強與性能好的代碼之上,你還應(yīng)當(dāng)努力使代碼盡可能地容易推理和維護。
注意: 對于編程來說,抽象不總是一種健康的東西——許多時候它可能在得到簡潔的同時增加復(fù)雜性。但是在這種情況下,我相信你的generator+Promise異步代碼要比其他的選擇健康得多。雖然有所有這些建議,你仍然要注意你的特殊情況,并為你和你的團隊做出合適的決策。
Generator 委托
在上一節(jié)中,我們展示了從generator內(nèi)部調(diào)用普通函數(shù),和它如何作為一種有用的技術(shù)來將實現(xiàn)細節(jié)(比如異步Promise流程)抽象出去。但是為這樣的任務(wù)使用普通函數(shù)的缺陷是,它必須按照普通函數(shù)的規(guī)則行動,也就是說它不能像generator那樣用yield
來暫停自己。
在你身上可能發(fā)生這樣的事情:你可能會試著使用我們的run(..)
幫助函數(shù),從一個generator中調(diào)用另個一generator。比如:
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()
內(nèi)部運行*foo()
。我們利用了這樣一個事實:我們早先定義的run(..)
返回一個promise,這個promise在generator運行至完成時才解析(或發(fā)生錯誤),所以如果我們從一個run(..)
調(diào)用中yield
出一個promise給另一個run(..)
,它就會自動暫停*bar()
直到*foo()
完成。
但這里有一個更好的辦法將*foo()
調(diào)用整合進*bar()
,它稱為yield
委托。yield
委托的特殊語法是:yield * __
(注意額外的*
)。讓它在我們前面的例子中工作之前,讓我們看一個更簡單的場景:
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() ..
,相似地,我也偏好——與關(guān)于這個話題的其他大多數(shù)文檔不同——說yield *foo()
而不是yield* foo()
。*
的擺放是純粹的風(fēng)格問題,而且要看你的最佳判斷。但我發(fā)現(xiàn)保持統(tǒng)一風(fēng)格很吸引人。
yield *foo()
委托是如何工作的?
首先,正如我們看到過的那樣,調(diào)用foo()
創(chuàng)建了一個 迭代器。然后,yield *
將(當(dāng)前*bar()
generator的) 迭代器 的控制委托/傳遞給這另一個*foo()
迭代器。
那么,前兩個it.next()
調(diào)用控制著*bar()
,但當(dāng)我們發(fā)起第三個it.next()
調(diào)用時,*foo()
就啟動了,而且這時我們控制的是*foo()
而非*bar()
。這就是為什么它稱為委托——*bar()
將它的迭代控制委托給*foo()
。
只要it
迭代器 的控制耗盡了整個*foo()
迭代器,它就會自動地將控制返回到*bar()
。
那么現(xiàn)在回到前面的三個順序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" );
// 通過`run(..)`“委托”到`*foo()`
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
這個代碼段和前面使用的版本的唯一區(qū)別是,使用了yield *foo()
而不是前面的yield run(foo)
。
注意: yield *
讓出了迭代控制,不是generator控制;當(dāng)你調(diào)用*foo()
generator時,你就yield
委托給它的 迭代器。但你實際上可以yield
委托給任何 迭代器;yield *[1,2,3]
將會消費默認(rèn)的[1,2,3]
數(shù)組值 迭代器。
為什么委托?
yield
委托的目的很大程度上是為了代碼組織,而且這種方式是與普通函數(shù)調(diào)用對稱的。
想象兩個分別提供了foo()
和bar()
方法的模塊,其中bar()
調(diào)用foo()
。它們倆分開的原因一般是由于為了程序?qū)⑺鼈冏鳛榉蛛x的程序來調(diào)用而進行的恰當(dāng)組織。例如,可能會有一些情況foo()
需要被獨立調(diào)用,而其他地方bar()
來調(diào)用foo()
。
由于這些完全相同的原因,將generator分開可以增強程序的可讀性,可維護性,與可調(diào)試性。從這個角度講,yield *
是一種快捷的語法,用來在*bar()
內(nèi)部手動地迭代*foo()
的步驟。
如果*foo()
中的步驟是異步的,這樣的手動方式可能會特別復(fù)雜,這就是為什么你可能會需要那個run(..)
工具來做它。正如我們已經(jīng)展示的,yield *foo()
消滅了使用run(..)
工具的子實例(比如run(foo)
)的需要。
委托消息
你可能想知道,這種yield
委托在除了與 迭代器 控制一起工作以外,是如何與雙向消息傳遞一起工作的。仔細查看下面這些通過yield
委托進進出出的消息流:
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)
調(diào)用之后的處理步驟:
- 值
3
被傳入(通過*bar
里的yield
委托)在*foo()
內(nèi)部等待中的yield "C"
表達式。 - 然后
*foo()
調(diào)用return "D"
,但是這個值不會一路返回到外面的it.next(3)
調(diào)用。 - 相反地,值
"D"
作為結(jié)果被發(fā)送到在*bar()
內(nèi)部等待中的yield *foo()
表示式——這個yield
委托表達式實質(zhì)上在*foo()
被耗盡之前一直被暫停著。所以"D"
被送到*bar()
內(nèi)部來讓它打印。 -
yield "E"
在*bar()
內(nèi)部被調(diào)用,而且值"E"
被讓出到外部作為it.next(3)
調(diào)用的結(jié)果。
從外部 迭代器(it
)的角度來看,在初始的generator和被委托的generator之間的控制沒有任何區(qū)別。
事實上,yield
委托甚至不必指向另一個generator;它可以僅被指向一個非generator的,一般的 iterable。比如:
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
注意這個例子與前一個之間,被接收/報告的消息的不同之處。
最驚人的是,默認(rèn)的array
迭代器 不關(guān)心任何通過next(..)
調(diào)用被發(fā)送的消息,所以值2
,3
,與4
實質(zhì)上被忽略了。另外,因為這個 迭代器 沒有明確的return
值(不像前面使用的*foo()
),所以yield *
表達式在它完成時得到一個undefined
。
異常也委托!
與yield
委托在兩個方向上透明地傳遞消息的方式相同,錯誤/異常也在雙向傳遞:
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
在這段代碼中有一些事情要注意:
- 但我們調(diào)用
it.throw(2)
時,它發(fā)送一個錯誤消息2
到*bar()
,而*bar()
將它委托至*foo()
,然后*foo()
來catch
它并平靜地處理。之后,yield "C"
把"C"
作為返回的value
發(fā)送回it.throw(2)
調(diào)用。 - 接下來值
"D"
被從*foo()
內(nèi)部throw
出來并傳播到*bar()
,*bar()
會catch
它并平靜地處理。然后yield "E"
把"E"
作為返回的value
發(fā)送回it.next(3)
調(diào)用。 - 接下來,一個異常從
*baz()
中throw
出來,而沒有被*bar()
捕獲——我們沒在外面catch
它——所以*baz()
和*bar()
都被設(shè)置為完成狀態(tài)。這段代碼結(jié)束后,即便有后續(xù)的next(..)
調(diào)用,你也不會得到值"G"
——它們的value
將返回undefined
。
異步委托
最后讓我們回到早先的多個順序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 run(foo)
不同的是,我們調(diào)用yield *foo()
就可以了。
在前一個版本的這個例子中,Promise機制(通過run(..)
控制的)被用于將值從*foo()
中的return r3
傳送到*bar()
內(nèi)部的本地變量r3
。現(xiàn)在,這個值通過yield *
機制直接返回。
除此以外,它們的行為是一樣的。
“遞歸”委托
當(dāng)然,yield
委托可以一直持續(xù)委托下去,你想連接多少步驟就連接多少。你甚至可以在具有異步能力的generator上“遞歸”使用yield
委托——一個yield
委托至自己的generator:
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 )
來調(diào)用,因為它支持用額外傳遞的參數(shù)來進行g(shù)enerator的初始化。然而,為了在這里高調(diào)展示yield *
的靈活性,我們使用了無參數(shù)的*bar()
。
這段代碼之后的處理步驟是什么?堅持住,它的細節(jié)要描述起來可是十分錯綜復(fù)雜:
-
run(bar)
啟動了*bar()
generator。 -
foo(3)
為*foo(..)
創(chuàng)建了 迭代器 并傳遞3
作為它的val
參數(shù)。 - 因為
3 > 1
,foo(2)
創(chuàng)建了另一個 迭代器 并傳遞2
作為它的val
參數(shù)。 - 因為
2 > 1
,foo(1)
又創(chuàng)建了另一個 迭代器 并傳遞1
作為它的val
參數(shù)。 -
1 > 1
是false
,所以我們接下來用值1
調(diào)用request(..)
,并得到一個代表第一個Ajax調(diào)用的promise。 - 這個promise被
yield
出來,回到*foo(2)
generator實例。 -
yield *
將這個promise傳出并回到*foo(3)
生成generator。另一個yield *
把這個promise傳出到*bar()
generator實例。而又有另一個yield *
把這個promise傳出到run(..)
工具,而它將會等待這個promise(第一個Ajax請求)再處理。 - 當(dāng)這個promise解析時,它的完成消息會被發(fā)送以繼續(xù)
*bar()
,*bar()
通過yield *
把消息傳遞進*foo(3)
實例,*foo(3)
實例通過yield *
把消息傳遞進*foo(2)
generator實例,*foo(2)
實例通過yield *
把消息傳給那個在*foo(3)
generator實例中等待的一般的yield
。 - 這第一個Ajax調(diào)用的應(yīng)答現(xiàn)在立即從
*foo(3)
generator實例中被return
,作為*foo(2)
實例中yield *
表達式的結(jié)果發(fā)送回來,并賦值給本地val
變量。 -
*foo(2)
內(nèi)部,第二個Ajax請求用request(..)
發(fā)起,它的promise被yield
回到*foo(1)
實例,然后一路yield *
傳播到run(..)
(回到第7步)。當(dāng)promise解析時,第二個Ajax應(yīng)答一路傳播回到*foo(2)
generator實例,并賦值到他本地的val
變量。 - 最終,第三個Ajax請求用
request(..)
發(fā)起,它的promise走出到run(..)
,然后它的解析值一路返回,最后被return
到在*bar()
中等待的yield *
表達式。
天!許多瘋狂的頭腦雜技,對吧?你可能想要把它通讀幾遍,然后抓點兒零食放松一下大腦!
Generator并發(fā)
正如我們在第一章和本章早先討論過的,另個同時運行的“進程”可以協(xié)作地穿插它們的操作,而且許多時候這可以產(chǎn)生非常強大的異步表達式。
坦白地說,我們前面關(guān)于多個generator并發(fā)穿插的例子,展示了這真的容易讓人糊涂。但我們也受到了啟發(fā),有些地方這種能力十分有用。
回想我們在第一章中看過的場景,兩個不同但同時的Ajax應(yīng)答處理需要互相協(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)绾卧谶@種場景下使用多generator呢?
// `request(..)` 是一個基于Promise的Ajax工具
var res = [];
function *reqData(url) {
res.push(
yield request( url )
);
}
注意: 我們將在這里使用兩個*reqData(..)
generator的實例,但是這和分別使用兩個不同generator的一個實例沒有區(qū)別;這兩種方式在道理上完全一樣的。我們過一會兒就會看到兩個generator的協(xié)調(diào)操作。
與不得不將res[0]
和res[1]
賦值手動排序不同,我們將使用協(xié)調(diào)過的順序,讓res.push(..)
以可預(yù)見的順序恰當(dāng)?shù)貙⒅捣旁陬A(yù)期的位置。如此被表達的邏輯會讓人感覺更干凈。
但是我們將如何實際安排這種互動呢?首先,讓我們手動實現(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(..)
的兩個實例都開始發(fā)起它們的Ajax請求,然后用yield
暫停。之后我們再p1
解析時繼續(xù)運行第一個實例,而后來的p2
的解析將會重啟第二個實例。以這種方式,我們使用Promise的安排來確保res[0]
將持有第一個應(yīng)答,而res[1]
持有第二個應(yīng)答。
但坦白地說,這是可怕的手動,而且它沒有真正讓generator組織它們自己,而那才是真正的力量。讓我們用不同的方法試一下:
// `request(..)` 是一個基于Promise的Ajax工具
var res = [];
function *reqData(url) {
var data = yield request( url );
// 傳遞控制權(quán)
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();
} );
好的,這看起來好些了(雖然仍然是手動),因為現(xiàn)在兩個*reqData(..)
的實例真正地并發(fā)運行了,而且(至少是在第一部分)是獨立的。
在前一個代碼段中,第二個實例在第一個實例完全完成之前沒有給出它的數(shù)據(jù)。但是這里,只要它們的應(yīng)答一返回這兩個實例就立即分別收到他們的數(shù)據(jù),然后每個實例調(diào)用另一個yield
來傳送控制。最后我們在Promise.all([ .. ])
的處理器中選擇用什么樣的順序繼續(xù)它們。
可能不太明顯的是,這種方式因其對稱性啟發(fā)了一種可復(fù)用工具的簡單形式。讓我們想象使用一個稱為runAll(..)
的工具:
// `request(..)` 是一個基于Promise的Ajax工具
var res = [];
runAll(
function*(){
var p1 = request( "http://some.url.1" );
// 傳遞控制權(quán)
yield;
res.push( yield p1 );
},
function*(){
var p2 = request( "http://some.url.2" );
// 傳遞控制權(quán)
yield;
res.push( yield p2 );
}
);
注意: 我們沒有包含runAll(..)
的實現(xiàn)代碼,不僅因為它長得無法行文,也因為它是一個我們已經(jīng)在先前的 run(..)
中實現(xiàn)的邏輯的擴展。所以,作為留給讀者的一個很好的補充性練習(xí),請你自己動手改進run(..)
的代碼,來使它像想象中的runAll(..)
那樣工作。另外,我的 asynquence 庫提供了一個前面提到過的runner(..)
工具,它內(nèi)建了這種能力,我們將在本書的附錄A中討論它。
這是runAll(..)
內(nèi)部的處理將如何操作:
- 第一個generator得到一個代表從
"http://some.url.1"
來的Ajax應(yīng)答,然后將控制權(quán)yield
回到runAll(..)
工具。 - 第二個generator運行,并對
"http://some.url.2"
做相同的事,將控制權(quán)yield
回到runAll(..)
工具。 - 第一個generator繼續(xù),然后
yield
出他的promisep1
。在這種情況下runAll(..)
工具和我們前面的run(..)
做同樣的事,它等待promise解析,然后繼續(xù)這同一個generator(沒有控制傳遞!)。當(dāng)p1
解析時,runAll(..)
使用解析值再一次繼續(xù)第一個generator,而后res[0]
得到它的值。在第一個generator完成之后,有一個隱式的控制權(quán)傳遞。 - 第二個generator繼續(xù),
yield
出它的promisep2
,并等待它的解析。一旦p2
解析,runAll(..)
使用這個解析值繼續(xù)第二個generator,于是res[1]
被設(shè)置。
在這個例子中,我們使用了一個稱為res
的外部變量來保存兩個不同的Ajax應(yīng)答的結(jié)果——這是我們的并發(fā)協(xié)調(diào)。
但是這樣做可能十分有幫助:進一步擴展runAll(..)
使它為多個generator實例提供 分享的 內(nèi)部的變量作用域,比如一個我們將在下面稱為data
的空對象。另外,它可以接收被yield
的非Promise值,并把它們交給下一個generator。
考慮這段代碼:
// `request(..)` 是一個基于Promise的Ajax工具
runAll(
function*(data){
data.res = [];
// 傳遞控制權(quán)(并傳遞消息)
var url1 = yield "http://some.url.2";
var p1 = request( url1 ); // "http://some.url.1"
// 傳遞控制權(quán)
yield;
data.res.push( yield p1 );
},
function*(data){
// 傳遞控制權(quán)(并傳遞消息)
var url2 = yield "http://some.url.1";
var p2 = request( url2 ); // "http://some.url.2"
// 傳遞控制權(quán)
yield;
data.res.push( yield p2 );
}
);
在這個公式中,兩個generator不僅協(xié)調(diào)控制傳遞,實際上還互相通信:通過data.res
,和交換url1
與url2
的值的yield
消息。這強大到不可思議!
這樣的認(rèn)識也是一種更為精巧的稱為CSP(Communicating Sequential Processes——通信順序處理)的異步技術(shù)的概念基礎(chǔ),我們將在本書的附錄B中討論它。
Thunks
至此,我們都假定從一個generator中yield
一個Promise——讓這個Promise使用像run(..)
這樣的幫助工具來推進generator——是管理使用generator的異步處理的最佳方法。明白地說,它是的。
但是我們跳過了一個被輕度廣泛使用的模式,為了完整性我們將簡單地看一看它。
在一般的計算機科學(xué)中,有一種老舊的前JS時代的概念,稱為“thunk”。我們不在這里贅述它的歷史,一個狹隘的表達是,thunk是一個JS函數(shù)——沒有任何參數(shù)——它連接并調(diào)用另一個函數(shù)。
換句話講,你用一個函數(shù)定義包裝函數(shù)調(diào)用——帶著它需要的所有參數(shù)——來 推遲 這個調(diào)用的執(zhí)行,而這個包裝用的函數(shù)就是thunk。當(dāng)你稍后執(zhí)行thunk時,你最終會調(diào)用那個原始的函數(shù)。
舉個例子:
function foo(x,y) {
return x + y;
}
function fooThunk() {
return foo( 3, 4 );
}
// 稍后
console.log( fooThunk() ); // 7
所以,一個同步的thunk是十分直白的。但是一個異步的thunk呢?我們實質(zhì)上可以擴展這個狹隘的thunk定義,讓它接收一個回調(diào)。
考慮這段代碼:
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(..)
參數(shù),因為它已經(jīng)預(yù)先制定了值3
和4
(分別為x
和y
)并準(zhǔn)備傳遞給foo(..)
。一個thunk只是在外面耐心地等待著它開始工作所需的最后一部分信息:回調(diào)。
但是你不會想要手動制造thunk。那么,讓我們發(fā)明一個工具來為我們進行這種包裝。
考慮這段代碼:
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(..)
)函數(shù)簽名希望它的回調(diào)的位置在最后,而其它的參數(shù)在這之前。這是一個異步JS函數(shù)的相當(dāng)普遍的“標(biāo)準(zhǔn)”。你可以稱它為“回調(diào)后置風(fēng)格”。如果因為某些原因你需要處理“回調(diào)優(yōu)先風(fēng)格”的簽名,你只需要制造一個使用args.unshift(..)
而非args.push(..)
的工具。
前面的thunkify(..)
公式接收foo(..)
函數(shù)的引用,和任何它所需的參數(shù),并返回thunk本身(fooThunk(..)
)。然而,這并不是你將在JS中發(fā)現(xiàn)的thunk的典型表達方式。
與thunkify(..)
制造thunk本身相反,典型的——可能有點兒讓人困惑的——thunkify(..)
工具將產(chǎn)生一個制造thunk的函數(shù)。
額...是的。
考慮這段代碼:
function thunkify(fn) {
return function() {
var args = [].slice.call( arguments );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
};
}
這里主要的不同之處是有一個額外的return function() { .. }
。這是它在用法上的不同:
var whatIsThis = thunkify( foo );
var fooThunk = whatIsThis( 3, 4 );
// 稍后
fooThunk( function(sum) {
console.log( sum ); // 7
} );
明顯地,這段代碼隱含的最大的問題是,whatIsThis
叫什么合適?它不是thunk,它是一個從foo(..)
調(diào)用生產(chǎn)thunk的東西。它是一種“thunk”的“工廠”。而且看起來沒有任何標(biāo)準(zhǔn)的意見來命名這種東西。
所以,我的提議是“thunkory”("thunk" + "factory")。于是,thunkify(..)
制造了一個thunkory,而一個thunkory制造thunks。這個道理與第三章中我的“promisory”提議是對稱的:
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(..)
期望的回調(diào)不是“錯誤優(yōu)先風(fēng)格”。當(dāng)然,“錯誤優(yōu)先風(fēng)格”更常見。如果foo(..)
有某種合理的錯誤發(fā)生機制,我們可以改變而使它期望并使用一個錯誤優(yōu)先的回調(diào)。后續(xù)的thunkify(..)
不會關(guān)心回調(diào)被預(yù)想成什么樣。用法的唯一區(qū)別是fooThunk1(function(err,sum){..
。
暴露出thunkory方法——而不是像早先的thunkify(..)
那樣將中間步驟隱藏起來——可能看起來像是沒必要的混亂。但是一般來講,在你的程序一開始就制造一些thunkory來包裝既存API的方法是十分有用的,然后你就可以在你需要thunk的時候傳遞并調(diào)用這些thunkory。這兩個區(qū)別開的步驟保證了功能上更干凈的分離。
來展示一下的話:
// 更干凈:
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有什么關(guān)系?
一般性地比較一下thunk和promise:它們是不能直接互換的,因為它們在行為上不是等價的。比起單純的thunk,Promise可用性更廣泛,而且更可靠。
但從另一種意義上講,它們都可以被看作是對一個值的請求,這個請求可能被異步地應(yīng)答。
回憶第三章,我們定義了一個工具來promise化一個函數(shù),我們稱之為Promise.wrap(..)
——我們本來也可以叫它promisify(..)
的!這個Promise化包裝工具不會生產(chǎn)Promise;它生產(chǎn)那些繼而生產(chǎn)Promise的promisories。這和我們當(dāng)前討論的thunkory和thunk是完全對稱的。
為了描繪這種對稱性,讓我們首先將foo(..)
的例子改為假定一個“錯誤優(yōu)先風(fēng)格”回調(diào)的形式:
function foo(x,y,cb) {
setTimeout( function(){
// 假定 `cb(..)` 是“錯誤優(yōu)先風(fēng)格”
cb( null, x + y );
}, 1000 );
}
現(xiàn)在,我們將比較thunkify(..)
和promisify(..)
(也就是第三章的Promise.wrap(..)
):
// 對稱的:構(gòu)建問題的回答者
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實質(zhì)上都是在問一個問題(一個值),thunk的fooThunk
和promise的fooPromise
分別代表這個問題的未來的答案。這樣看來,對稱性就清楚了。
帶著這個視角,我們可以看到為了異步而yield
Promise的generator,也可以為異步而yield
thunk。我們需要的只是一個更聰明的run(..)
工具(就像以前一樣),它不僅可以尋找并連接一個被yield
的Promise,而且可以給一個被yield
的thunk提供回調(diào)。
考慮這段代碼:
function *foo() {
var val = yield request( "http://some.url.1" );
console.log( val );
}
run( foo );
在這個例子中,request(..)
既可以是一個返回一個promise的promisory,也可以是一個返回一個thunk的thunkory。從generator的內(nèi)部代碼邏輯的角度看,我們不關(guān)心這個實現(xiàn)細節(jié),這就它強大的地方!
所以,request(..)
可以使以下任何一種形式:
// promisory `request(..)` (見第三章)
var request = Promise.wrap( ajax );
// vs.
// thunkory `request(..)`
var request = thunkify( ajax );
最后,作為一個讓我們早先的run(..)
工具支持thunk的補丁,我們可能會需要這樣的邏輯:
// ..
// 我們收到了一個回調(diào)嗎?
else if (typeof next.value == "function") {
return new Promise( function(resolve,reject){
// 使用一個錯誤優(yōu)先回調(diào)調(diào)用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 );
}
);
}
現(xiàn)在,我們generator既可以調(diào)用promisory來yield
Promise,也可以調(diào)用thunkory來yield
thunk,而不論那種情況,run(..)
都將處理這個值并等待它的完成,以繼續(xù)generator。
在對稱性上,這兩個方式是看起來相同的。然而,我們應(yīng)當(dāng)指出這僅僅從Promise或thunk表示延續(xù)generator的未來值的角度講是成立的。
從更高的角度講,與Promise被設(shè)計成的那樣不同,thunk沒有提供,它們本身也幾乎沒有任何可靠性和可組合性的保證。在這種特定的generator異步模式下使用一個thunk作為Promise的替代品是可以工作的,但與Promise提供的所有好處相比,這應(yīng)當(dāng)被看做是一種次理想的方法。
如果你有選擇,那就偏向yield pr
而非yield th
。但是使run(..)
工具可以處理兩種類型的值本身沒有什么問題。
注意: 在我們將要在附錄A中討論的,我的 asynquence 庫中的runner(..)
工具,可以處理yield
的Promise,thunk和 asynquence 序列。
前ES6時代的Generator
我希望你已經(jīng)被說服了,generator是一個異步編程工具箱里的非常重要的增強工具。但它是ES6中的新語法,這意味著你不能像填補Promise(它只是新的API)那樣填補generator。那么如果我們不能奢望忽略前ES6時代的瀏覽器,我們該如何將generator帶到瀏覽器中呢?
對所有ES6中的新語法的擴展,有一些工具——稱呼他們最常見的名詞是轉(zhuǎn)譯器(transpilers),也就是轉(zhuǎn)換編譯器(trans-compilers)——它們會拿起你的ES6語法,并轉(zhuǎn)換為前ES6時代的等價代碼(但是明顯地變難看了!)。所以,generator可以被轉(zhuǎn)譯為具有相同行為但可以在ES5或以下版本進行工作的代碼。
但是怎么做到的?yield
的“魔法”聽起來不像是那么容易轉(zhuǎn)譯的。在我們早先的基于閉包的 迭代器 例子中,實際上提示了一種解決方法。
手動變形
在我們討論轉(zhuǎn)譯器之前,讓我們延伸一下,在generator的情況下如何手動轉(zhuǎn)譯。這不僅是一個學(xué)院派的練習(xí),因為這樣做實際上可以幫助我們進一步理解它們?nèi)绾喂ぷ鳌?/p>
考慮這段代碼:
// `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" );
第一個要注意的事情是,我們?nèi)匀恍枰粋€可以被調(diào)用的普通的foo()
函數(shù),而且它仍然需要返回一個 迭代器。那么讓我們來畫出非generator的變形草圖:
function foo(url) {
// ..
// 制造并返回 iterator
return {
next: function(v) {
// ..
},
throw: function(e) {
// ..
}
};
}
var it = foo( "http://some.url.1" );
下一個需要注意的地方是,generator通過掛起它的作用域/狀態(tài)來施展它的“魔法”,但我們可以用函數(shù)閉包來模擬。為了理解如何寫出這樣的代碼,我們將先用狀態(tài)值注釋generator不同的部分:
// `request(..)` 是一個支持Promise的Ajax工具
function *foo(url) {
// 狀態(tài) *1*
try {
console.log( "requesting:", url );
var TMP1 = request( url );
// 狀態(tài) *2*
var val = yield TMP1;
console.log( val );
}
catch (err) {
// 狀態(tài) *3*
console.log( "Oops:", err );
return false;
}
}
注意: 為了更準(zhǔn)去地講解,我們使用TMP1
變量將val = yield request..
語句分割為兩部分。request(..)
發(fā)生在狀態(tài)*1*
,而將完成值賦給val
發(fā)生在狀態(tài)*2*
。在我們將代碼轉(zhuǎn)換為非generator的等價物后,我們就可以擺脫中間的TMP1
。
換句話所,*1*
是初始狀態(tài),*2*
是request(..)
成功的狀態(tài),*3*
是request(..)
失敗的狀態(tài)。你可能會想象額外的yield
步驟將如何編碼為額外的狀態(tài)。
回到我們被轉(zhuǎn)譯的generator,讓我們在這個閉包中定義一個變量state
,用它來追蹤狀態(tài):
function foo(url) {
// 管理 generator 狀態(tài)
var state;
// ..
}
現(xiàn)在,讓我們在閉包內(nèi)部定義一個稱為process(..)
的內(nèi)部函數(shù),它用switch
語句來處理各種狀態(tài)。
// `request(..)` 是一個支持Promise的Ajax工具
function foo(url) {
// 管理 generator 狀態(tài)
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中每種狀態(tài)都在switch
語句中有它自己的case
。每當(dāng)我們需要處理一個新狀態(tài)時,process(..)
就會被調(diào)用。我們一會就回來討論它如何工作。
對任何generator范圍的變量聲明(val
),我們將它們移動到process(..)
外面的var
聲明中,這樣它們就可以在process(..)
的多次調(diào)用中存活下來。但是“塊兒作用域”的err
變量僅在*3*
狀態(tài)下需要,所以我們將它留在原處。
在狀態(tài)*1*
,與yield request(..)
相反,我們return request(..)
。在終結(jié)狀態(tài)*2*
,沒有明確的return
,所以我們僅僅return;
也就是return undefined
。在終結(jié)狀態(tài)*3*
,有一個return false
,我們保留它。
現(xiàn)在我們需要定義 迭代器 函數(shù)的代碼,以便人們恰當(dāng)?shù)卣{(diào)用process(..)
:
function foo(url) {
// 管理 generator 狀態(tài)
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) {
// 初始狀態(tài)
if (!state) {
state = 1;
return {
done: false,
value: process()
};
}
// 成功地讓出繼續(xù)值
else if (state == 1) {
state = 2;
return {
done: true,
value: process( v )
};
}
// generator 已經(jīng)完成了
else {
return {
done: true,
value: undefined
};
}
},
"throw": function(e) {
// 在狀態(tài) *1* 中,有唯一明確的錯誤處理
if (state == 1) {
state = 3;
return {
done: true,
value: process( e )
};
}
// 否則,是一個不會被處理的錯誤,所以我們僅僅把它扔回去
else {
throw e;
}
}
};
}
這段代碼如何工作?
- 第一個對 迭代器 的
next()
調(diào)用將把gtenerator從未初始化的狀態(tài)移動到狀態(tài)1
,然后調(diào)用process()
來處理這個狀態(tài)。request(..)
的返回值是一個代表Ajax應(yīng)答的promise,它作為value
屬性從next()
調(diào)用被返回。 - 如果Ajax請求成功,第二個
next(..)
調(diào)用應(yīng)當(dāng)送進Ajax的應(yīng)答值,它將我們的狀態(tài)移動到2
。process(..)
再次被調(diào)用(這次它被傳入Ajax應(yīng)答的值),而從next(..)
返回的value
屬性將是undefined
。 - 然而,如果Ajax請求失敗,應(yīng)當(dāng)用錯誤調(diào)用
throw(..)
,它將狀態(tài)從1
移動到3
(而不是2
)。process(..)
再一次被調(diào)用,這詞被傳入了錯誤的值。這個case
返回false
,所以false
作為throw(..)
調(diào)用返回的value
屬性。
從外面看——也就是僅僅與 迭代器 互動——這個普通的foo(..)
函數(shù)與*foo(..)
generator的工作方式是一樣的。所以我們有效地將ES6 generator“轉(zhuǎn)譯”為前ES6可兼容的!
然后我們就可以手動初始化我們的generator并控制它的迭代器——調(diào)用var it = foo("..")
和it.next(..)
等等——或更好地,我們可以將它傳遞給我們先前定義的run(..)
工具,比如run(foo,"..")
。
自動轉(zhuǎn)譯
前面的練習(xí)——手動編寫從ES6 generator到前ES6的等價物的變形過程——教會了我們generator在概念上是如何工作的。但是這種變形真的是錯綜復(fù)雜,而且不能很好地移植到我們代碼中的其他generator上。手動做這些工作是不切實際的,而且將會把generator的好處完全抵消掉。
但走運的是,已經(jīng)存在幾種工具可以自動地將ES6 generator轉(zhuǎn)換為我們在前一節(jié)延伸出的東西。它們不僅幫我們做力氣活兒,還可以處理幾種我們敷衍而過的情況。
一個這樣的工具是regenerator(https://facebook.github.io/regenerator/),由Facebook的聰明伙計們開發(fā)的。
如果我們用regenerator來轉(zhuǎn)譯我們前面的generator,這就是產(chǎn)生的代碼(在編寫本文時):
// `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]]);
});
這和我們的手動推導(dǎo)有明顯的相似性,比如switch
/case
語句,而且我們甚至可以看到,val
被拉到了閉包外面,正如我們做的那樣。
當(dāng)然,一個代價是這個generator的轉(zhuǎn)譯需要一個幫助工具庫regeneratorRuntime
,它持有全部管理一個普通generator/迭代器 所需的可復(fù)用邏輯。它的許多模板代碼看起來和我們的版本不同,但即便如此,概念還是可以看到的,比如使用context$1$0.next = 4
追蹤generator的下一個狀態(tài)。
主要的結(jié)論是,generator不僅限于ES6+的環(huán)境中才有用。一旦你理解了它的概念,你可以在你的所有代碼中利用他們,并使用工具將代碼變形為舊環(huán)境兼容的。
這比使用Promise
API的填補來實現(xiàn)前ES6的Promise要做更多的工作,但是努力完全是值得的,因為對于以一種可推理的,合理的,看似同步的順序風(fēng)格來表達異步流程控制來說,generator實在是好太多了。
一旦你適應(yīng)了generator,你將永遠不會回到面條般的回調(diào)地獄了!
復(fù)習(xí)
generator是一種ES6的新函數(shù)類型,它不像普通函數(shù)那樣運行至完成。相反,generator可以暫停在一種中間完成狀態(tài)(完整地保留它的狀態(tài)),而且它可以從暫停的地方重新開始。
這種暫停/繼續(xù)的互換是一種協(xié)作而非搶占,這意味著generator擁有的唯一能力是使用yield
關(guān)鍵字暫停它自己,而且控制這個generator的 迭代器 擁有的唯一能力是繼續(xù)這個generator(通過next(..)
)。
yield
/next(..)
的對偶不僅是一種控制機制,它實際上是一種雙向消息傳遞機制。一個yield ..
表達式實質(zhì)上為了等待一個值而暫停,而下一個next(..)
調(diào)用將把值(或隱含的undefined
)傳遞回這個暫停的yield
表達式。
與異步流程控制關(guān)聯(lián)的generator的主要好處是,在一個generator內(nèi)部的代碼以一種自然的同步/順序風(fēng)格表達一個任務(wù)的各個步驟的序列。這其中的技巧是我們實質(zhì)上將潛在的異步處理隱藏在yield
關(guān)鍵字的后面——將異步處理移動到控制generator的 迭代器 代碼中。
換句話說,generator為異步代碼保留了順序的,同步的,阻塞的代碼模式,這允許我們的大腦更自然地推理代碼,解決了基于回調(diào)的異步產(chǎn)生的兩個關(guān)鍵問題中的一個。