感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取
如果你寫(xiě)過(guò)任何數(shù)量相當(dāng)?shù)腏avaScript,這就不是什么秘密:異步編程是一種必須的技能。管理異步的主要機(jī)制曾經(jīng)是函數(shù)回調(diào)。
然而,ES6增加了一種新特性:Promise,來(lái)幫助你解決僅使用回調(diào)來(lái)管理異步的重大缺陷。另外,我們可以重溫generator(前一章中提到的)來(lái)看看一種將兩者組合的模式,它是JavaScript中異步流程控制編程向前邁出的重要一步。
Promises
讓我們辨明一些誤解:Promise不是回調(diào)的替代品。Promise提供了一種可信的中介機(jī)制 —— 也就是,在你的調(diào)用代碼和將要執(zhí)行任務(wù)的異步代碼之間 —— 來(lái)管理回調(diào)。
另一種考慮Promise的方式是作為一種事件監(jiān)聽(tīng)器,你可以在它上面注冊(cè)監(jiān)聽(tīng)一個(gè)通知你任務(wù)何時(shí)完成的事件。它是一個(gè)僅被觸發(fā)一次的事件,但不管怎樣可以被看作是一個(gè)事件。
Promise可以被鏈接在一起,它們可以是一系列順序的、異步完成的步驟。與all(..)
方法(用經(jīng)典的術(shù)語(yǔ)將,叫“門(mén)”)和race(..)
方法(用經(jīng)典的術(shù)語(yǔ)將,叫“閂”)這樣的高級(jí)抽象一起,promise鏈可以提供一種異步流程控制的機(jī)制。
還有另外一種概念化Promise的方式是,將它看作一個(gè) 未來(lái)值,一個(gè)與時(shí)間無(wú)關(guān)的值的容器。無(wú)論底層的值是否是最終值,這種容器都可以被同樣地推理。觀測(cè)一個(gè)Promise的解析會(huì)在這個(gè)值準(zhǔn)備好的時(shí)候?qū)⑺槿〕鰜?lái)。換言之,一個(gè)Promise被認(rèn)為是一個(gè)同步函數(shù)返回值的異步版本。
一個(gè)Promise只可能擁有兩種解析結(jié)果:完成或拒絕,并帶有一個(gè)可選的信號(hào)值。如果一個(gè)Promise被完成,這個(gè)最終值稱(chēng)為一個(gè)完成值。如果它被拒絕,這個(gè)最終值稱(chēng)為理由(也就是“拒絕的理由”)。Promise只可能被解析(完成或拒絕)一次。任何其他的完成或拒絕的嘗試都會(huì)被簡(jiǎn)單地忽略,一旦一個(gè)Promise被解析,它就成為一個(gè)不可被改變的值(immutable)。
顯然,有幾種不同的方式可以來(lái)考慮一個(gè)Promise是什么。沒(méi)有一個(gè)角度就它自身來(lái)說(shuō)是完全充分的,但是每一個(gè)角度都提供了整體的一個(gè)方面。這其中的要點(diǎn)是,它們?yōu)閮H使用回調(diào)的異步提供了一個(gè)重大的改進(jìn),也就是它們提供了順序、可預(yù)測(cè)性、以及可信性。
創(chuàng)建與使用 Promises
要構(gòu)建一個(gè)promise實(shí)例,可以使用Promise(..)
構(gòu)造器:
var p = new Promise( function pr(resolve,reject){
// ..
} );
Promise(..)
構(gòu)造器接收一個(gè)單獨(dú)的函數(shù)(pr(..)
),它被立即調(diào)用并以參數(shù)值的形式收到兩個(gè)控制函數(shù),通常被命名為resolve(..)
和reject(..)
。它們被這樣使用:
- 如果你調(diào)用
reject(..)
,promise就會(huì)被拒絕,而且如果有任何值被傳入reject(..)
,它就會(huì)被設(shè)置為拒絕的理由。 - 如果你不使用參數(shù)值,或任何非promise值調(diào)用
resolve(..)
,promise就會(huì)被完成。 - 如果你調(diào)用
resolve(..)
并傳入另一個(gè)promise,這個(gè)promise就會(huì)簡(jiǎn)單地采用 —— 要么立即要么最終地 —— 這個(gè)被傳入的promise的狀態(tài)(不是完成就是拒絕)。
這里是你通常如何使用一個(gè)promise來(lái)重構(gòu)一個(gè)依賴(lài)于回調(diào)的函數(shù)調(diào)用。假定你始于使用一個(gè)ajax(..)
工具,它期預(yù)期要調(diào)用一個(gè)錯(cuò)誤優(yōu)先風(fēng)格的回調(diào):
function ajax(url,cb) {
// 發(fā)起請(qǐng)求,最終調(diào)用 `cb(..)`
}
// ..
ajax( "http://some.url.1", function handler(err,contents){
if (err) {
// 處理ajax錯(cuò)誤
}
else {
// 處理成功的`contents`
}
} );
你可以將它轉(zhuǎn)換為:
function ajax(url) {
return new Promise( function pr(resolve,reject){
// 發(fā)起請(qǐng)求,最終不是調(diào)用 `resolve(..)` 就是調(diào)用 `reject(..)`
} );
}
// ..
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
// 處理成功的 `contents`
},
function rejected(reason){
// 處理ajax的錯(cuò)誤reason
}
);
Promise擁有一個(gè)方法then(..)
,它接收一個(gè)或兩個(gè)回調(diào)函數(shù)。第一個(gè)函數(shù)(如果存在的話(huà))被看作是promise被成功地完成時(shí)要調(diào)用的處理器。第二個(gè)函數(shù)(如果存在的話(huà))被看作是promise被明確拒絕時(shí),或者任何錯(cuò)誤/異常在解析的過(guò)程中被捕捉到時(shí)要調(diào)用的處理器。
如果這兩個(gè)參數(shù)值之一被省略或者不是一個(gè)合法的函數(shù) —— 通常你會(huì)用null
來(lái)代替 —— 那么一個(gè)占位用的默認(rèn)等價(jià)物就會(huì)被使用。默認(rèn)的成功回調(diào)將傳遞它的完成值,而默認(rèn)的錯(cuò)誤回調(diào)將傳播它的拒絕理由。
調(diào)用then(null,handleRejection)
的縮寫(xiě)是catch(handleRejection)
。
then(..)
和catch(..)
兩者都自動(dòng)地構(gòu)建并返回另一個(gè)promise實(shí)例,它被鏈接在原本的promise上,接收原本的promise的解析結(jié)果 —— (實(shí)際被調(diào)用的)完成或拒絕處理器返回的任何值。考慮如下代碼:
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
return contents.toUpperCase();
},
function rejected(reason){
return "DEFAULT VALUE";
}
)
.then( function fulfilled(data){
// 處理來(lái)自于原本的promise的處理器中的數(shù)據(jù)
} );
在這個(gè)代碼段中,我們要么從fulfilled(..)
返回一個(gè)立即值,要么從rejected(..)
返回一個(gè)立即值,然后在下一個(gè)事件周期中這個(gè)立即值被第二個(gè)then(..)
的fulfilled(..)
接收。如果我們返回一個(gè)新的promise,那么這個(gè)新promise就會(huì)作為解析結(jié)果被納入與采用:
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
return ajax(
"http://some.url.2?v=" + contents
);
},
function rejected(reason){
return ajax(
"http://backup.url.3?err=" + reason
);
}
)
.then( function fulfilled(contents){
// `contents` 來(lái)自于任意一個(gè)后續(xù)的 `ajax(..)` 調(diào)用
} );
要注意的是,在第一個(gè)fulfilled(..)
中的一個(gè)異常(或者promise拒絕)將 不會(huì) 導(dǎo)致第一個(gè)rejected(..)
被調(diào)用,因?yàn)檫@個(gè)處理僅會(huì)應(yīng)答第一個(gè)原始的promise的解析。取代它的是,第二個(gè)then(..)
調(diào)用所針對(duì)的第二個(gè)promise,將會(huì)收到這個(gè)拒絕。
在上面的代碼段中,我們沒(méi)有監(jiān)聽(tīng)這個(gè)拒絕,這意味著它會(huì)為了未來(lái)的觀察而被靜靜地保持下來(lái)。如果你永遠(yuǎn)不通過(guò)調(diào)用then(..)
或catch(..)
來(lái)觀察它,那么它將會(huì)成為未處理的。有些瀏覽器的開(kāi)發(fā)者控制臺(tái)可能會(huì)探測(cè)到這些未處理的拒絕并報(bào)告它們,但是這不是有可靠保證的;你應(yīng)當(dāng)總是觀察promise拒絕。
注意: 這只是Promise理論和行為的簡(jiǎn)要概覽。要進(jìn)行更加深入的探索,參見(jiàn)本系列的 異步與性能 的第三章。
Thenables
Promise是Promise(..)
構(gòu)造器的純粹實(shí)例。然而,還存在稱(chēng)為 thenable 的類(lèi)promise對(duì)象,它通常可以與Promise機(jī)制協(xié)作。
任何帶有then(..)
函數(shù)的對(duì)象(或函數(shù))都被認(rèn)為是一個(gè)thenable。任何Promise機(jī)制可以接受與采用一個(gè)純粹的promise的狀態(tài)的地方,都可以處理一個(gè)thenable。
Thenable基本上是一個(gè)一般化的標(biāo)簽,標(biāo)識(shí)著任何由除了Promise(..)
構(gòu)造器之外的其他系統(tǒng)創(chuàng)建的類(lèi)promise值。從這個(gè)角度上講,一個(gè)thenable沒(méi)有一個(gè)純粹的Promise那么可信。例如,考慮這個(gè)行為異常的thenable:
var th = {
then: function thener( fulfilled ) {
// 永遠(yuǎn)會(huì)每100ms調(diào)用一次`fulfilled(..)`
setInterval( fulfilled, 100 );
}
};
如果你收到這個(gè)thenable并使用th.then(..)
將它鏈接,你可能會(huì)驚訝地發(fā)現(xiàn)你的完成處理器被反復(fù)地調(diào)用,而普通的Promise本應(yīng)該僅僅被解析一次。
一般來(lái)說(shuō),如果你從某些其他系統(tǒng)收到一個(gè)聲稱(chēng)是promise或thenable的東西,你不應(yīng)當(dāng)盲目地相信它。在下一節(jié)中,我們將會(huì)看到一個(gè)ES6 Promise的工具,它可以幫助解決信任的問(wèn)題。
但是為了進(jìn)一步理解這個(gè)問(wèn)題的危險(xiǎn),讓我們考慮一下,在 任何 一段代碼中的 任何 對(duì)象,只要曾經(jīng)被定義為擁有一個(gè)稱(chēng)為then(..)
的方法就都潛在地會(huì)被誤認(rèn)為是一個(gè)thenable —— 當(dāng)然,如果和Promise一起使用的話(huà) —— 無(wú)論這個(gè)東西是否有意與Promise風(fēng)格的異步編碼有一絲關(guān)聯(lián)。
在ES6之前,對(duì)于稱(chēng)為then(..)
的方法從來(lái)沒(méi)有任何特別的保留措施,正如你能想象的那樣,在Promise出現(xiàn)在雷達(dá)屏幕上之前就至少有那么幾種情況,它已經(jīng)被選擇為方法的名稱(chēng)了。最有可能用錯(cuò)thenable的情況就是使用then(..)
的異步庫(kù)不是嚴(yán)格兼容Promise的 —— 在市面上有好幾種。
這份重?fù)?dān)將由你來(lái)肩負(fù):防止那些將被誤認(rèn)為一個(gè)thenable的值被直接用于Promise機(jī)制。
Promise
API
Promise
API還為處理Promise提供了一些靜態(tài)方法。
Promise.resolve(..)
創(chuàng)建一個(gè)被解析為傳入的值的promise。讓我們將它的工作方式與更手動(dòng)的方法比較一下:
var p1 = Promise.resolve( 42 );
var p2 = new Promise( function pr(resolve){
resolve( 42 );
} );
p1
和p2
將擁有完全相同的行為。使用一個(gè)promise進(jìn)行解析也一樣:
var theP = ajax( .. );
var p1 = Promise.resolve( theP );
var p2 = new Promise( function pr(resolve){
resolve( theP );
} );
提示: Promise.resolve(..)
就是前一節(jié)提出的thenable信任問(wèn)題的解決方案。任何你還不確定是一個(gè)可信promise的值 —— 它甚至可能是一個(gè)立即值 —— 都可以通過(guò)傳入Promise.resolve(..)
來(lái)進(jìn)行規(guī)范化。如果這個(gè)值已經(jīng)是一個(gè)可識(shí)別的promise或thenable,它的狀態(tài)/解析結(jié)果將簡(jiǎn)單地被采用,將錯(cuò)誤行為與你隔絕開(kāi)。如果相反它是一個(gè)立即值,那么它將會(huì)被“包裝”進(jìn)一個(gè)純粹的promise,以此將它的行為規(guī)范化為異步的。
Promise.reject(..)
創(chuàng)建一個(gè)立即被拒絕的promise,與它的Promise(..)
構(gòu)造器對(duì)等品一樣:
var p1 = Promise.reject( "Oops" );
var p2 = new Promise( function pr(resolve,reject){
reject( "Oops" );
} );
雖然resolve(..)
和Promise.resolve(..)
可以接收一個(gè)promise并采用它的狀態(tài)/解析結(jié)果,但是reject(..)
和Promise.reject(..)
不會(huì)區(qū)分它們收到什么樣的值。所以,如果你使用一個(gè)promise或thenable進(jìn)行拒絕,這個(gè)promise/thenable本身將會(huì)被設(shè)置為拒絕的理由,而不是它底層的值。
Promise.all([ .. ])
接收一個(gè)或多個(gè)值(例如,立即值,promise,thenable)的數(shù)組。它返回一個(gè)promise,這個(gè)promise會(huì)在所有的值完成時(shí)完成,或者在這些值中第一個(gè)被拒絕的值出現(xiàn)時(shí)被立即拒絕。
使用這些值/promises:
var p1 = Promise.resolve( 42 );
var p2 = new Promise( function pr(resolve){
setTimeout( function(){
resolve( 43 );
}, 100 );
} );
var v3 = 44;
var p4 = new Promise( function pr(resolve,reject){
setTimeout( function(){
reject( "Oops" );
}, 10 );
} );
讓我們考慮一下使用這些值的組合,Promise.all([ .. ])
如何工作:
Promise.all( [p1,p2,v3] )
.then( function fulfilled(vals){
console.log( vals ); // [42,43,44]
} );
Promise.all( [p1,p2,v3,p4] )
.then(
function fulfilled(vals){
// 永遠(yuǎn)不會(huì)跑到這里
},
function rejected(reason){
console.log( reason ); // Oops
}
);
Promise.all([ .. ])
等待所有的值完成(或第一個(gè)拒絕),而Promise.race([ .. ])
僅會(huì)等待第一個(gè)完成或拒絕。考慮如下代碼:
// 注意:為了避免時(shí)間的問(wèn)題誤導(dǎo)你,
// 重建所有的測(cè)試值!
Promise.race( [p2,p1,v3] )
.then( function fulfilled(val){
console.log( val ); // 42
} );
Promise.race( [p2,p4] )
.then(
function fulfilled(val){
// 永遠(yuǎn)不會(huì)跑到這里
},
function rejected(reason){
console.log( reason ); // Oops
}
);
警告: 雖然 Promise.all([])
將會(huì)立即完成(沒(méi)有任何值),但是 Promise.race([])
將會(huì)被永遠(yuǎn)掛起。這是一個(gè)奇怪的不一致,我建議你應(yīng)當(dāng)永遠(yuǎn)不要使用空數(shù)組調(diào)用這些方法。
Generators + Promises
將一系列promise在一個(gè)鏈條中表達(dá)來(lái)代表你程序的異步流程控制是 可能 的。考慮如如下代碼:
step1()
.then(
step2,
step1Failed
)
.then(
function step3(msg) {
return Promise.all( [
step3a( msg ),
step3b( msg ),
step3c( msg )
] )
}
)
.then(step4);
但是對(duì)于表達(dá)異步流程控制來(lái)說(shuō)有更好的選項(xiàng),而且在代碼風(fēng)格上可能比長(zhǎng)長(zhǎng)的promise鏈更理想。我們可以使用在第三章中學(xué)到的generator來(lái)表達(dá)我們的異步流程控制。
要識(shí)別一個(gè)重要的模式:一個(gè)generator可以yield出一個(gè)promise,然后這個(gè)promise可以使用它的完成值來(lái)推進(jìn)generator。
考慮前一個(gè)代碼段,使用generator來(lái)表達(dá):
function *main() {
try {
var ret = yield step1();
}
catch (err) {
ret = yield step1Failed( err );
}
ret = yield step2( ret );
// step 3
ret = yield Promise.all( [
step3a( ret ),
step3b( ret ),
step3c( ret )
] );
yield step4( ret );
}
從表面上看,這個(gè)代碼段要比前一個(gè)promise鏈等價(jià)物要更繁冗。但是它提供了更加吸引人的 —— 而且重要的是,更加容易理解和閱讀的 —— 看起來(lái)同步的代碼風(fēng)格(“return”值的=
賦值操作,等等),對(duì)于try..catch
錯(cuò)誤處理可以跨越那些隱藏的異步邊界使用來(lái)說(shuō)就更是這樣。
為什么我們要與generator一起使用Promise?不用Promise進(jìn)行異步generator編碼當(dāng)然是可能的。
Promise是一個(gè)可信的系統(tǒng),它將普通的回調(diào)和thunk中發(fā)生的控制倒轉(zhuǎn)(參見(jiàn)本系列的 異步與性能)反轉(zhuǎn)回來(lái)。所以組合Promise的可信性與generator中代碼的同步性有效地解決了回調(diào)的主要缺陷。另外,像Promise.all([ .. ])
這樣的工具是一個(gè)非常美好、干凈的方式 —— 在一個(gè)generator的一個(gè)yield
步驟中表達(dá)并發(fā)。
那么這種魔法是如何工作的?我們需要一個(gè)可以運(yùn)行我們generator的 運(yùn)行器(runner),接收一個(gè)被yield
出來(lái)的promise并連接它,讓它要么使用成功的完成推進(jìn)generator,要么使用拒絕的理由向generator拋出異常。
許多具備異步能力的工具/庫(kù)都有這樣的“運(yùn)行器”;例如,Q.spawn(..)
和我的asynquence中的runner(..)
插件。這里有一個(gè)獨(dú)立的運(yùn)行器來(lái)展示這種處理如何工作:
function run(gen) {
var args = [].slice.call( arguments, 1), it;
it = gen.apply( this, args );
return Promise.resolve()
.then( function handleNext(value){
var next = it.next( value );
return (function handleResult(next){
if (next.done) {
return next.value;
}
else {
return Promise.resolve( next.value )
.then(
handleNext,
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
})( next );
} );
}
注意: 這個(gè)工具的更豐富注釋的版本,參見(jiàn)本系列的 異步與性能。另外,由各種異步庫(kù)提供的這種運(yùn)行工具通常要比我們?cè)谶@里展示的東西更強(qiáng)大。例如,asynquence的runner(..)
可以處理被yield
的promise、序列、thunk、以及(非promise的)間接值,給你終極的靈活性。
于是現(xiàn)在運(yùn)行早先代碼段中的*main()
就像這樣容易:
run( main )
.then(
function fulfilled(){
// `*main()` 成功地完成了
},
function rejected(reason){
// 噢,什么東西搞錯(cuò)了
}
);
實(shí)質(zhì)上,在你程序中的任何擁有多于兩個(gè)異步步驟的流程控制邏輯的地方,你就可以 而且應(yīng)當(dāng) 使用一個(gè)由運(yùn)行工具驅(qū)動(dòng)的promise-yielding generator來(lái)以一種同步的風(fēng)格表達(dá)流程控制。這樣做將產(chǎn)生更易于理解和維護(hù)的代碼。
這種“讓出一個(gè)promise推進(jìn)generator”的模式將會(huì)如此常見(jiàn)和如此強(qiáng)大,以至于ES6之后的下一個(gè)版本的JavaScript幾乎可以確定將會(huì)引入一中新的函數(shù)類(lèi)型,它無(wú)需運(yùn)行工具就可以自動(dòng)地執(zhí)行。我們將在第八章中講解async function
(正如它們期望被稱(chēng)呼的那樣)。
復(fù)習(xí)
隨著JavaScript在它被廣泛采用過(guò)程中的日益成熟與成長(zhǎng),異步編程越發(fā)地成為關(guān)注的中心。對(duì)于這些異步任務(wù)來(lái)說(shuō)回調(diào)并不完全夠用,而且在更精巧的需求面前全面崩塌了。
可喜的是,ES6增加了Promise來(lái)解決回調(diào)的主要缺陷之一:在可預(yù)測(cè)的行為上缺乏可信性。Promise代表一個(gè)潛在異步任務(wù)的未來(lái)完成值,跨越同步和異步的邊界將行為進(jìn)行了規(guī)范化。
但是,Promise與generator的組合才完全揭示了這樣做的好處:將我們的異步流程控制代碼重新安排,將難看的回調(diào)漿糊(也叫“地獄”)弱化并抽象出去。
目前,我們可以在各種異步庫(kù)的運(yùn)行器的幫助下管理這些交互,但是JavaScript最終將會(huì)使用一種專(zhuān)門(mén)的獨(dú)立語(yǔ)法來(lái)支持這種交互模式!