你不知道JS:異步
第三章:Promises
在第二章,我們指出了采用回調來表達異步和管理并發時的兩種主要不足:缺乏序列化和可信度。既然我們對這些問題更熟悉了,是時候將注意力集中到解決這些問題的模式上了。
我們想要處理的第一個問題是控制權反轉,信任很難維持,脆弱且容易丟失。
回想一下我們把程序的延續包裹在回調函數中,把回調交給其它方(甚至可能是外部代碼),然后祈禱能夠正確地調用回調函數。
我們這樣做的原因是我們想說,“這個是在當前步驟結束之后,后來發生的”。
但要是我們能夠反逆轉控制權反轉呢?要是能夠讓第三方返回一個結果,讓我們知道它的任務結束時間,然后我們的代碼決定接下來該做什么呢?而不是把程序的延續交給第三方。
這一范例稱為Promises。
隨著諸如開發人員和規范制定人員之類的人不顧一切地尋求解決代碼/設計中回調地獄錯亂問題的方法,Promises將會撼動整個JS世界。事實上,JS/DOM中新加入的API大多數都是建立在Promises上的。那么深入學習Promises可能是個不錯的注意,不是嗎?
注意: 本章中”立刻(immediately)“一詞將會反復提及,通常指某些Promise的解析行為。然而,從本質上來說,”立刻“一詞是根據作業隊列(Job queue)(見第一章)來說的,不是現在(now)層面上嚴格同步的意思。
Promise是什么?(What Is a Promise?)
當開發人員學習一種新的技術或者模式時,第一步通常是”給我看代碼!“對我們而言,直接進入(譯者注:指直接看代碼)并學習很自然。
但結果是,只關注API的話,就會丟失一些抽象概念。Promise是這樣的一種工具,如果不理解它是做什么的,只是學會使用API的話,使用起來會很痛苦。
因此,在我展示Promise代碼之前,我想從概念上完完整整地解釋Promise到底是什么。我希望這能夠指導你更好地將Promise理論整合到自己的異步流中。
明確這點,讓我們看一下兩個關于Promise是什么的不同類比。
未來值(Future Value)
假設這個場景:我走到快餐店的柜臺,點了一個奶酪漢堡,我遞給收銀員$1.47的零錢。通過下單和支付,我已經作了一次請求,希望有值(奶酪漢堡)返回。我已經開始了一次交易。
但通常,奶酪漢堡并不是立刻就能給我。收銀員給了我一個代替奶酪漢堡的東西:一個有訂單號的收據。這個訂單號是一個IOU(”i owe you
“)的承諾,確保最后我能拿到我的奶酪漢堡。
我拿著我的收據和訂單號。我知道它代表著我未來的奶酪漢堡,因此我再也不用擔心了--除了我很餓之外!
當我在等的時候,我還可以做其它事,比如給我的朋友發短信說,”嗨,你能和我一起吃午餐嗎?我要吃個奶酪漢堡。“
即使還沒拿到手,我已經開始推演我的未來奶酪漢堡。我的大腦之所以能這樣做是因為它把訂單號當成了奶酪漢堡占位符。本質上這個占位符使得值與時間無關。它是未來值。
最終,我聽到了,”113號!“我很高興地拿著訂單走向柜臺。把訂單遞給收銀員后,我拿到了我的奶酪漢堡。
換句話說,一旦我的未來值準備好了,我就可以拿承諾值(value-promise)換真正的值(value)了。
但還可能有另一種結果。他們叫了我的訂單號,但是當我去取奶酪漢堡時,收銀員很遺憾地告訴我,”很抱歉,我們恰巧賣完了奶酪漢堡“。拋開客戶沮喪的場景來看,我們可以看到未來值的一個重要特點:它們既可能預示著成功,也可能預示著失敗。
每次我點一個奶酪漢堡,我都知道可能最終我會得到一個,或者很遺憾地聽到奶酪漢堡賣完了,不得不另尋其它作為午餐了。
注意: 在代碼中,事情并不是如此簡單,因為比方來說,訂單號可能永遠都不會被叫到,此時我們就無限地處在了懸而未決的狀態。我們稍后處理這種情形。
現在值和以后值(Values Now and Later)
如果應用到代碼中,聽起來似乎太抽象了。讓我們更具體一點。
然而,在我們以這種方式引入Promise的工作原理前,我們將從已知的代碼中--回調--去探究如何處理這些未來值的。
當你寫代碼來推演一個值的時候,比如對一個number
進行數學運算。不論是否意識到,關于那個值,你已經假定了一些重要的東西,即它已經是一個具體的現在值:
var x, y = 2;
console.log( x + y ); // NaN <-- because `x` isn't set yet
x+y
的操作假定x
和y
已經設置好值了。簡而言之,我們假定x
和y
已經被解析了(resolved)。
期望+
操作符本身能夠檢測和等到x
和y
都解析完(即準備好,然后去進行+
的操作)顯然沒什么意義。如果不同的語句現在完成,其它的以后完成,這樣會使得程序混亂,對嗎?
如果一個語句(或者兩個都)還沒有完成,你該如何推演它們之間的關系呢?如果語句2依賴語句1的完成,只會有兩個結果:要么語句1立馬完成(right now),一切OK,要么語句1還沒完成,進而語句2也將失敗。
如果覺得聽起來有點耳熟,很好!
讓我們回到x+y
的數學操作。想象以一種方式說,”將x
和y
相加,如果其中任一個還沒準備好,只需等等直至二者都準備好了。盡可能快的將二者相加。“
你的腦袋可能立即跳到了回調。OK,那么...
function add(getX,getY,cb) {
var x, y;
getX( function(xVal){
x = xVal;
// both are ready?
if (y != undefined) {
cb( x + y ); // send along sum
}
} );
getY( function(yVal){
y = yVal;
// both are ready?
if (x != undefined) {
cb( x + y ); // send along sum
}
} );
}
// `fetchX()` and `fetchY()` are sync or async
// functions
add( fetchX, fetchY, function(sum){
console.log( sum ); // that was easy, huh?
} );
花幾分鐘來完全領會這段代碼的美(或不足)。
不可否認,盡管有點難看,但是這種異步模式中還是有值得注意的地方。
在那段代碼中,我們把x
和y
視作未來值,并且表示操作的add(..)
并不關心x
和y
是否都立刻準備好了。換句話說,它統一了現在和以后,如此我們就可以依賴add(..)
操作生成的一個可預測的結果。
通過使用時序一致的add(..)
---現在和以后的行為都保持一致--使得異步代碼更容易推演了。
更通俗一點來說:為了統一處理現在和以后,我們把它們都變成了以后。所有的操作都變成異步的了。
當然,這種簡單的基于回調不是太令人滿意。推演未來值而不需要擔心時間維度,即值是否可用,對于這一好處而言,這只是一小步。
承諾值(Promise Value)
本章中,我們肯定會探討Promise的更多細節--所以,如果有點困惑,別擔心--但讓我們簡單看看如何采用Promise
來實現x+y
的操作:
function add(xPromise,yPromise) {
// `Promise.all([ .. ])` takes an array of promises,
// and returns a new promise that waits on them
// all to finish
return Promise.all( [xPromise, yPromise] )
// when that promise is resolved, let's take the
// received `X` and `Y` values and add them together.
.then( function(values){
// `values` is an array of the messages from the
// previously resolved promises
return values[0] + values[1];
} );
}
// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
add( fetchX(), fetchY() )
// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(..)` to wait for the
// resolution of that returned promise.
.then( function(sum){
console.log( sum ); // that was easier!
} );
這段代碼中有兩層Promise。
fetchX()
和fetchY()
是直接調用的,返回值(promises!)傳入add(..)
。這些promise代表的潛在值可能是現在或者以后準備好,但每個promise統一了這一行為。我們以跟時間無關的方式推演(reason about)x
和y
。他們是未來值。
第二層是add(..)
創建并返回的promise(通過Promise.all([ .. ])
),在此通過調用then(..)
等待。當add(..)
操作完成后,sum
未來值就準備好了,之后我們就可以輸出它了。我們隱藏了add(..)
中等待x
和y
未來值的內部邏輯。
注意: 在add(..)
內部,Promise.all([ .. ])
創建了一個promise(等待promiseX
和promiseY
解析)。.then(..)
的鏈式調用創建了一個promise,return values[0] + values[1]
立即得到解析(相加的結果)。因此,add(..)
調用之后的鏈式then(..)
調用--代碼末尾--實際上是對返回的第二個promise進行操作,而不是對Promise.all([ .. ])
創建的第一個。另外,盡管我們沒有對第二個then(..)
進行鏈式操作,它其實也生成了一個promise。這種Promise的鏈式調用會在本章后面作更詳盡的說明。
就像奶酪漢堡訂單一樣,一個Promise的解析結果可能不是成功(fulfillment)而是失敗(rejection)的。不像一個成功的promise,其值總是程序化的(programmatic,譯者注:指通過程序設定),一個失敗值--通常稱為“失敗原因”--既可通過程序邏輯直接設定,又可由隱式的運行異常導致。
通過Promise,then(..)
調用實際能夠接受兩個函數,一個用作成功(fulfillment),第二個用作失敗(rejection):
add( fetchX(), fetchY() )
.then(
// fullfillment handler
function(sum) {
console.log( sum );
},
// rejection handler
function(err) {
console.error( err ); // bummer!
}
);
如果獲取x
或者y
出錯,或者在相加過程中有什么出錯了,則add(..)
返回的promise被置為失敗狀態,傳入then(..)
的第二個回調錯誤處理函數將接受promise返回的失敗值。
因為Promise從外面封裝了跟時間無關的狀態--等待潛在值的成功或失敗--Promise本身是跟時間無關的,因此Promise可以以可預測的方式組織(組合)起來,不管時間或結果。
此外,一旦Promise解析了,其狀態永遠不變--自那一時刻起,變成了不可更改的值--可以按需多次監聽。
注意: 因為Promise一旦解析,就無法從外面更改其狀態了,這樣把值傳遞給第三方就很安全,因為無法無意或惡意地修改它。尤其是當有多個當事方監聽這一個Promise的解析結果時更是如此。一方無法影響另一方對Promise解析的監聽。不可變性聽起來似乎像一個學術課題,但它其實是Promise設計中十分重要的方面之一,不能隨意忽略。
那是理解Promise所需的最強大和最重要的概念之一。有一定實踐經驗后,你也能通過難看的回調組合來專門實現同樣的效果,但那并不是一個真正有效的策略,尤其是你必須一次又一次地這么做。
Promise是一種很容易封裝和構成未來值的可重復性機制。
完成事件(Completion Event)
正如我們剛剛看到的,一個Promise的行為就像一個未來值。但還可以采用另一種方式來思考Promise:在異步任務的兩步或多步中--一個時序的this-then-that--作為一種流控制機制。
假設調用函數foo(..)
來執行某個任務。我們不知道任何細節,也不關心。可能是立即完成的,也可能花一些時間。
我們只需簡單地知道foo(..)
什么時候完成,以便我們能夠轉到下一個任務中去。換句話說,我們想要通過某種方式來通知我們foo(..)
已經完成了,以便于繼續做其它事。
在傳統JS中,如果你需要監聽某個通知(notification),你可能會想到事件。因此,我們可以重構通知需求為需要監聽foo(..)
發出的完成(或者繼續)事件。
注意: 稱之為“完成事件”還是“繼續事件”取決于你自己。關注點更應該放在foo(..)
發生了什么或者foo(..)
后會發生什么。這兩個角度都是準確和有用的。事件通知告訴我們foo(..)
已經完成了,同樣也可以說是能夠進行下一步了。事件通知時調用的傳入的回調函數本身就是我們之前提到的延續(continuation)。因為完成事件更關注于foo(..)
,此時我們也更關注于它。在本篇其余部分,會傾向于采用完成事件。
采用回調函數,“通知”就是任務(foo(..)
)運行的回調。但采用Promise時,我們轉變一下關系,希望我們可以從foo(..)
中監聽事件,當被通知的時候,進行相應的處理。
首先,考慮如下偽代碼:
foo(x) {
// start doing something that could take a while
}
foo( 42 )
on (foo "completion") {
// now we can do the next step!
}
on (foo "error") {
// oops, something went wrong in `foo(..)`
}
我們調用foo(..)
,然后設置了兩個事件監聽器,一個監聽"completion"
事件,一個監聽"error"
事件--foo(..)
調用的兩個可能的結果。本質上,foo(..)
并不知道調用代碼已經訂閱了這些事件,很好的實現了關注點分離。
很不幸的是,這種代碼需要JS環境的一些“魔法”,但并不存在(并且可能有點不切實際)。以下是我們在JS中以更自然的方式實現的代碼:
function foo(x) {
// start doing something that could take a while
// make a `listener` event notification
// capability to return
return listener;
}
var evt = foo( 42 );
evt.on( "completion", function(){
// now we can do the next step!
} );
evt.on( "failure", function(err){
// oops, something went wrong in `foo(..)`
} );
foo(..)
顯式地創建了事件訂閱功能用作返回,然后調用代碼接收它并
針對它注冊了兩個事件處理函數。
很明顯,這是從正常的回調導向的代碼反轉(控制權),也是特意這么做的。foo(..)
返回一個稱之為evt
的事件功能,用它來接收回調函數。
但是如果你回想一下第二章,回調函數本身代表控制權反轉(inversion of control)。因此,反轉回調模式通常稱之為反轉的反轉(inversion of inversion),或者不反轉控制權(uninversion of control)--在我們需要以它為主的地方恢復調用代碼的控制權。
這樣做的一大好處就是代碼的多個獨立部分可以獲得事件監聽能力,當foo(..)
完成的時候,它們可以獨立地接到通知并進行后續步驟:
var evt = foo( 42 );
// let `bar(..)` listen to `foo(..)`'s completion
bar( evt );
// also, let `baz(..)` listen to `foo(..)`'s completion
baz( evt );
不反轉控制權(Uninversion of control)能夠實現更好的關注點分離,bar(..)
和baz(..)
不需要關心foo(..)
是如何調用的。同樣的,foo(..)
也不需要知道或者關心bar(..)
和baz(..)
的存在或者foo(..)
完成時bar(..)
和baz(..)
正等著被通知。
本質上來說,這個evt
對象是不同關注點之間的一個中立的第三方協商。
Promise “事件”(Promise "Events")
現在,你可能猜想,evt
事件監聽能力是對Promise的一個模擬。
在基于Promise的方式中,之前的代碼會讓foo(..)
生成并返回一個Promise
實例,那個promise之后會被傳入bar(..)
和baz(..)
。
注意: 我們監聽的Promise解析“事件”并不是嚴格意義上的事件(盡管某些地方看起來像事件的行為),并且通常也不稱為"completion"
或者"error"
。反而,我們采用then(..)
來注冊一個then(..)
事件。或者更準確一點,then(..)
注冊了"fulfillment"
和/或"rejection"
事件,盡管我們在代碼中無法明確地看到這些東西。
考慮如下代碼:
function foo(x) {
// start doing something that could take a while
// construct and return a promise
return new Promise( function(resolve,reject){
// eventually, call `resolve(..)` or `reject(..)`,
// which are the resolution callbacks for
// the promise.
} );
}
var p = foo( 42 );
bar( p );
baz( p );
注意: 采用new Promise( function(..){ .. } )
的這種模式通常稱為“暴露構造函數(revealing constructor)”(譯者注:指Promise構造函數暴露了內部的功能,但只針對構造promise對象的代碼)。傳入的函數立即執行(不是像傳入then(..)
中的回調函數異步推遲執行的),該函數接收兩個參數,此處我們命名為resolve
和reject
。這兩個是promise的解析函數。resolve(..)
通常標志著成功(fulfillment),reject(..)
通常標志著失敗(rejection)。
你可能在猜想bar(..)
和baz(..)
的內部是什么樣子:
function bar(fooPromise) {
// listen for `foo(..)` to complete
fooPromise.then(
function(){
// `foo(..)` has now finished, so
// do `bar(..)`'s task
},
function(){
// oops, something went wrong in `foo(..)`
}
);
}
// ditto for `baz(..)`
Promise解析并不一定像我們檢查Promise并作為未來值時一樣,需要隨之傳送一則信息。它可以簡單地用作流控制信號,正如前段代碼中用到的一樣。
另一種實現方法是:
function bar() {
// `foo(..)` has definitely finished, so
// do `bar(..)`'s task
}
function oopsBar() {
// oops, something went wrong in `foo(..)`,
// so `bar(..)` didn't run
}
// ditto for `baz()` and `oopsBaz()`
var p = foo( 42 );
p.then( bar, oopsBar );
p.then( baz, oopsBaz );
注意: 如果你之前已經看過基于Promise的代碼,你可能認為最后兩行代碼可以采用鏈式方式寫作p.then( .. ).then( .. )
,而不是p.then(..); p.then(..)
。注意,結果完全不同。現在看起來,差異還不是很明顯,但它其實是一種不同于迄今為止我們見過的異步模式:分離/分叉(splitting/forking)。別擔心!后面我們會講。
我們用promise來控制bar(..)
和baz(..)
何時執行,而不是把p
promise傳入到bar(..)
和baz(..)
中。主要的差異在于錯誤處理。
在第一段代碼中,不論foo(..)
成功還是失敗,bar(..)
都會調用,如果被通知到foo(..)
失敗了,由bar(..)
處理自己的回退邏輯。很明顯,baz(..)
也是一樣。
在第二段代碼中,只在foo(..)
成功的時候調用bar(..)
,否則調用oopsBar(..)
。baz(..)
也是一樣。
本質上來說,沒有哪一個是更恰當的。很多情況下,一個要比另一個更好。
無論哪一種情形,foo(..)
返回的promise p
都是用來控制接下來干什么。
此外,這兩段代碼在最后都對同一個promise p
調用了兩次 then(..)
,是為了提前說明一點,即Promise(一旦解析)會永遠保持同樣的解析結果(fulfillment或rejection)不變,可以隨后按需任意監聽多次。
無論何時,一旦p
解析了,下一步總是一樣的,不論現在還是以后。
Thenable 鴨子類型(Thenable Duck Typing)
在Promise領域中,一個重要的細節是如何判斷某個值是否是一個真正的Promise。或者更直接一點,它是一個和Promise行為類似的值嗎?
鑒于Promise是通過new Promise(..)
語法構建的,你可能認為p instanceof Promise
是一個可以接受的檢查。但不幸的是,有一萬種理由來證明這完全不夠。
主要地,你可能從另一個瀏覽器窗口(iframe等)接收一個Promise,它可能有一個自己的Promise,不同于當前窗口/frame,此時,采用類型檢查(即p instanceof Promise
)無法識別Promise實例。
此外,庫或者框架可能選擇提供自己的Promise,而不是采用原生的ES6 Promise實現。事實上,你可能一直在沒有Promise的舊瀏覽器上通過庫使用Promise。
當我們在本章后面討論Promise解析過程的時候,一個non-genuine-but-Promise-like(不是真正的但是像Promise)的值仍然能夠識別和理解是很重要的,其原因會變得更明顯。但目前,只記住我的話,這是謎團中很重要的一部分。
同樣的,識別一個Promise(或者一個表現得像Promise)的方法是定義一個稱為"thenable"的東西,它是指任何一個有then(..)
方法的對象或者函數。假定任何此類的值是一個Promise-conforming thenable(符合Promise的thenable)。
假定一個值的“類型”是基于其形狀(存在什么屬性),通常這種“類型檢查”稱為“鴨子類型”--“如果看起來像鴨子,叫起來也像鴨子,那一定就是鴨子”。因此thenable的鴨子類型檢查大概就是這樣:
if (
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
) {
// assume it's a thenable!
}
else {
// not a thenable
}
Yuck(一臉嫌棄的眼神)!撇開這種難看的實現邏輯不談(很多地方都采用這種方法),有些更深層次和更麻煩的東西還在后頭。
如果你想用任一個恰巧有then(..)
函數的對象/函數來fulfill一個Promise(譯者注:指將Promise置為成功狀態),但你并不想將它當做一個Promise/thenable,很不走運,因為它會被自動識別為thenable并且采用一些特殊的規則(見本章后面)。
如果是在你沒意識到它有一個then(..)
方法時,更是如此。比如:
var o = { then: function(){} };
// make `v` be `[[Prototype]]`-linked to `o`
var v = Object.create( o );
v.someStuff = "cool";
v.otherStuff = "not so cool";
v.hasOwnProperty( "then" ); // false
v
看起來一點也不像一個Promise或者thenable。它只是個有些屬性的普通對象。你可能只是想將它和其它普通對象一樣傳遞出去。
但你不知道的是,v
也是[[Prototype]]
鏈于另一個對象o
,而o
恰巧有一個then(..)
方法。因此,thenable鴨子類型檢查會認為v
是一個thenable。呃。
甚至不需要像下面直接這樣做:
Object.prototype.then = function(){};
Array.prototype.then = function(){};
var v1 = { hello: "world" };
var v2 = [ "Hello", "World" ];
v1
和v2
都會被假定為thenable。你無法控制或者預測哪些代碼有意或者無意地在Object.prototype
,Array.prototype
或者其它原生原型上添加了then(..)
方法。如果指定的函數并沒有調用任一個參數(譯者注:指then(..)
方法中的兩個參數,一個是resolve,一個是reject)作為回調。那么任一個解析該值的Promise將會永遠靜默掛起!瘋狂吧。
聽起來不可思議或者不可能?也許吧。
但是請記住,在ES6之前,有幾個知名的非Promise的庫已經存在于社區了,并且恰巧其中有then(..)
方法。其中一些庫選擇重新命名他們的方法來避免沖突(糟透了!)。其他的只是簡單地降級到“與基于Promise的編程不兼容”的狀態來應對他們無力改變使其不受影響這一局面。
標準決定劫持之前非保留的--并且聽起來完全通用的--then
屬性名,這意味著,任何值(或者其代理)在過去、現在以及將來,都不能有then(..)
方法出現,不論是有意的還是無意的,否則在Promise系統中,該值會被誤認為thenable,進而很可能引起bug,不易追蹤。
警告: 在Promise識別中,我不想以thenable的鴨子類型檢查結束。還可以有其它選項,比如“品牌化(branding)”或者“反品牌化(anti-branding)”;似乎我們選擇了最壞的一種折衷。但情況并非一團糟。thenable鴨子類型也是有用的,之后我們會看到。記住,如果把不是Promise的東西錯誤地識別為Promise,那樣鴨子類型檢查就會很危險。
Promise信任問題(Promise Trust)
我們已經舉了兩個有力的例子(譯者注:即作為未來值和完成事件),用來從不同方面解釋Promise能為我們的異步代碼做些什么。但如果我們止步于此,我們會錯過Promise模式建立起來的一個最重要的特性:信任。
盡管我們已經在代碼中很清楚地展開探究了未來值和完成事件,我們還不是很清楚Promise是如何設計的,能夠解決我們在第二章“信任問題”一節中提出的所有控制權反轉信任問題。只要稍加深究,我們就能在異步編程時重拾第二章中失去的信心!
讓我們回顧一下只采用回調編程的信任問題。當傳遞一個回調給utilityfoo(..)
,可能:
- 太早調用回調
- 太晚調用回調(或者從不調用)
- 調用太多或者太少
- 無法傳遞任何必要的環境/參數
- 掩蓋可能發生的任何錯誤/異常
Promise的諸多特性就是針對這些問題提供了有用的,經得起考驗的答案。
太早調用(Calling Too Early)
這個問題主要在于代碼是否會引起像Zalgo效應(見第二章),即一些任務有時是同步完成的,有時是異步完成的,這樣會導致競態。
Promise從定義上來說就不會受此影響。因為即使是一個立即的fulfilled Promise(比如new Promise(function(resolve){ resolve(42); })
)也無法同步監聽到。
也就是說,當你在一個Promise上調用then(..)
方法時,即使Promise已經解析了,你提供給then(..)
的回調函數總是異步執行的(要想知道更多,參考第一章“作業”一節)。
再也不需要插入setTimeout(..,0)
的hack了,Promise自動阻止了Zalgo。
太晚調用(Calling Too Late)
與前一點類似,當Promise創建時調用resolve(..)
或者reject(..)
,Promise then(..)
注冊的監聽回調是自動調度的。這些調度的回調函數會在下一個異步時刻觸發(詳見第一章中的“作業”)。
同步監聽是不可能的,因此以這種方式運行的一串同步任務“推遲”(從效果上來說)另一個回調的執行是不可能的。也就是說,當一個Promise解析完,所有注冊在then(..)
上的回調函數都會在下一個異步窗口(再次,見第一章的“作業”)按序立即(immediately)調用,并且任一個回調內部發生的東西不會影響/推遲其它回調的執行。
例如:
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// A B C
根據Promise定義的運行原理,此處"C"
無法中斷并領先"B"
。
Promise調度的怪癖(Promise Scheduling Quirks)
然而,有一點必須要注意,在相對順序無法可靠預測時,兩個不同Promise的鏈式回調的調度有許多細節需要考慮。
如果兩個promise p1
和p2
已經解析完了,那么p1.then(..); p2.then(..)
最終p1
的回調會在p2
的回調之前被調用。但是有一些不可思議的情況,比如下面這個:
var p3 = new Promise( function(resolve,reject){
resolve( "B" );
} );
var p1 = new Promise( function(resolve,reject){
resolve( p3 );
} );
var p2 = new Promise( function(resolve,reject){
resolve( "A" );
} );
p1.then( function(v){
console.log( v );
} );
p2.then( function(v){
console.log( v );
} );
// A B <-- not B A as you might expect
對此,我們之后會細講,但如你所見,p1
并不是立即值解析的,而是另一個promise p3
,p3
解析的是值B
。這一特定行為是把p3
打開傳入到p1
中,但是異步的,因此在異步作業隊列(見第一章)中,p1
的回調是在p2
回調后面的。
為了避免這種微妙的噩夢,你絕不要依賴跨Promise來實現回調的排序/調度。實際上,一個好的方法是在多個回調的順序很重要的時候,不要像這樣編寫代碼。盡可能避免。
從不調用回調(Never Calling the Callback)
這是很常見的一個問題。可以用Promise采用多種方式解決。
首先。沒有什么(甚至是一個JS錯誤)能夠阻止Promise通知你它的解析結果(如果已經解析完了)。如果你為Promise同時注冊了fulfillment和rejection回調,待Promise解析完成后,總有一個會被調用。
當然,如果回調函數本身有JS錯誤,你可能看不到期望的結果,但回調函數已經執行過了。我們接下來會講在回調中如何接收錯誤,因為即使是那些也不會被掩蓋。
但要是Promise本身永不解析呢?即使是那種情況,Promise也提供了解決方法,用一種更高級的抽象,稱為“競爭(race)”:
// a utility for timing out a Promise
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}
// setup a timeout for `foo()`
Promise.race( [
foo(), // attempt `foo()`
timeoutPromise( 3000 ) // give it 3 seconds
] )
.then(
function(){
// `foo(..)` fulfilled in time!
},
function(err){
// either `foo()` rejected, or it just
// didn't finish in time, so inspect
// `err` to know which
}
);
這種Promise超時模式還有更多細節值得探究,之后會討論。
重要的是,我們可以確保有一個信號作為foo()
的結果,防止我們的程序被無限地掛起。
調用太少或太多次(Calling Too Few or Too Many Times)
從定義上看,就是回調被調用的次數。“太少”就是零調用,和我們之前說過的“從不”一個意思。
“太多”很容易解釋,Promise被定義為只能解析一次。如果由于某些原因,Promise的創建代碼試圖多次調用resolve(..)
或者reject(..)
,或者兩個都調用,Promise只會接受第一個解析,并靜默地忽略之后的嘗試。
因為Promise只能解析一次,任一個then(..)
注冊的回調也只能調用一次(對每個而言)。
當然,如果你不止一次地注冊同一個回調,(例如,p.then(f); p.then(f);
),則注冊多少次就執行多少次。響應函數只能調用一次并不能阻止你搬起石頭砸自己的腳。
無法傳遞任何參數/環境(Failing to Pass Along Any Parameters/Environment)
Promise最多只有一個解析值(fulfillment或者rejection)。
如果你不顯式地解析一個值,就會像傳統JS一樣,為undefined
。但是一旦有值,則總會傳到所有注冊(fulfillment或者rejection)的回調中去,不論是現在的還是將來的。
記住:如果調用resolve(..)
或者reject(..)
時的參數有多個,除了第一個參數,所有后面的參數都會被默認忽略。看起來似乎有點違背我們之前描述的保證,其實并不是,因為這是對Promise機制的一個無效應用。其它一些無效的API應用(比如多次調用resolve(..)
)也是同樣受到保護的,因此Promise的行為是一致的。
如果你想傳入多個值,必須把它們包在另一個你傳入的單值里,比如一個array
或者一個object
。
至于環境,JS函數中總是保存著定義時的作用域閉包,因此,它們當然
可以訪問任何你提供的環境狀態。當然,只有回調的設計中也是這樣的,因此這并不是Promise帶來的額外好處--但能夠保證我們可以依賴它。
掩蓋任何錯誤/異常(Swallowing Any Errors/Exceptions)
從基本意義上來說,這是之前觀點的重述。如果用一個原因(reason)(即錯誤信息)來將一個Promise置為失敗狀態,這個值(譯者注:指reason)就會被傳到rejection回調中去。
但此處還有更大的東西。在創建Promise過程中的任一時刻,或者在監聽解析結果的時候,如果發生了JS異常錯誤,比如TypeError
或者ReferenceError
,異常會被捕獲,并且會強制Promise變成失敗狀態。
比如:
var p = new Promise( function(resolve,reject){
foo.bar(); // `foo` is not defined, so error!
resolve( 42 ); // never gets here :(
} );
p.then(
function fulfilled(){
// never gets here :(
},
function rejected(err){
// `err` will be a `TypeError` exception object
// from the `foo.bar()` line.
}
);
發生自foo.bar()
的JS異常變成了一個Promise rejection,你可以捕獲它并進行響應。
這是個重要的細節,因為能夠有效地解決另一個潛在的Zalgo情形,即錯誤可能創建一個同步的響應而非錯誤(nonerrors)則是異步的。Promise甚至把JS異常也轉為異步的了,因此能夠極大地減少競態的發生。
但如果Promise被置為成功狀態(fulfilled)了,但是在監聽過程中(在一個then(..)
注冊回調中)發生了JS異常,會發生什么呢?即使能捕獲這些異常,在更深入了解之前,你可能會對處理異常的方式感到驚訝。
var p = new Promise( function(resolve,reject){
resolve( 42 );
} );
p.then(
function fulfilled(msg){
foo.bar();
console.log( msg ); // never gets here :(
},
function rejected(err){
// never gets here either :(
}
);
等等,似乎foo.bar()
的異常被掩蓋了。別擔心,并沒有。但是更深層次的東西出問題了,即我們無法監聽這個異常了。p.then(..)
本身返回了另一個promise,這個promise會因TypeError
異常而被置為失敗狀態(rejected)。
為什么沒有直接調用已經定義好的錯誤處理函數呢?表面上看起來似乎很合邏輯。但會違背一條重要原則,即一旦解析,Promise就是不可改變的。p
已經由值42
置為成功狀態,因此之后不可能僅僅因為監聽p
的解析中有一個異常而改為失敗狀態。
除了違背這一原則外,這樣的行為也可能造成很大破壞,假設在promise p
上有許多then(..)
注冊回調,因為有些會調用,而有些不會,這樣會導致原因不清不楚。
可信賴的Promise?(Trustable Promise?)
基于Promise模式建立信任還有最后一個細節。
毫無疑問,你已經注意到Promise并沒有擺脫回調。它們只是改變了回調傳入的地方。我們從foo(..)
中得到一個東西(看起來是一個真正的Promise),然后把回調傳入其中,而不是把回調傳入到foo(..)
中。
但為什么這比只使用回調更值得信賴呢?我們如何確保得到的東西真的是可信賴的Promise呢?我們所相信的(僅僅因為我們已經相信它了)是不是基本上都是空中樓閣?
關于Promise,一個很重要但經常被忽略的細節是,Promise對這個問題也有一個解決方案。即包含在原生ES6 Promise
中的Promise.resolve(..)
。
如果你向Promise.resolve(..)
中傳入一個立即值、非Promise值、非thenable值,你會得到一個以該值將狀態置為成功的promise。換句話說,以下兩個promise p1
和p2
行為基本上一致:
var p1 = new Promise( function(resolve,reject){
resolve( 42 );
} );
var p2 = Promise.resolve( 42 );
但是如果你向Promise.resolve(..)
中傳入一個真正的Promise,你只會得到同一個promise:
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true
更重要的是,如果你向Promise.resolve(..)
中傳入一個非Promise的thenable值。它會試圖拆析(unwrap)這個值,并且會一直持續直至抽取到一個具體的non-Promise-like值。
回想一下我們之前討論的thenable?
如下:
var p = {
then: function(cb) {
cb( 42 );
}
};
// this works OK, but only by good fortune
p
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// never gets here
}
);
這個p
是個thenable。但不是一個真正的Promise。幸運的是,這是合理的,因為絕大多數都是這樣的。但要是你得到的是這樣的呢:
var p = {
then: function(cb,errcb) {
cb( 42 );
errcb( "evil laugh" );
}
};
p
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// oops, shouldn't have run(本不該運行的啊!)
console.log( err ); // evil laugh
}
);
這個p
是一個thenable,但并沒有如promise表現得那么好。它是惡意的嗎?或者僅僅是忘記了Promise是如何運行的?說實話,沒關系。無論哪一種情形(即以上兩個例子)都是不可信任的。
然而,我們可以將這兩個版本的p
傳入到Promise.resolve(..)
中,最終會得到我們期望的標準、安全的結果。
Promise.resolve( p )
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// never gets here
}
);
Promise.resolve(..)
會接收任何thenable,然后將其拆析(unwrap)直至獲得一個非thenable值。但是從Promise.resolve(..)
,你會得到一個真正的promise,一個你可以信賴的promise。如果你傳入的已經是個真正的promise,只會原樣返回,因此,通過Promise.resolve(..)
過濾來獲取信任一點壞處也沒有。
因此,假設我們正在調用foo(..)
utility,我們不確定它的返回值是否是正常的Promise,但我們知道它至少是個thenable。Promise.resolve(..)
會給我們一個值得信賴的Promise包裝用作鏈式調用:
// don't just do this:
foo( 42 )
.then( function(v){
console.log( v );
} );
// instead, do this:
Promise.resolve( foo( 42 ) )
.then( function(v){
console.log( v );
} );
注意: 通過Promise.resolve(..)
包裝任何函數返回值(thenable或者其它)的另一個好處是,很容易將一個函數調用標準化為一個表現良好的異步任務。如果foo(42)
有時返回一個立即值,有時返回一個Promise,Promise.resolve( foo(42) )
能夠確保它總是一個Promise返回值。避免Zalgo讓代碼更好。
信任建立(Trust Built)
希望前面的討論完全“resolve”(雙關,既指解決,又指Promise中的resolve)你心中的疑惑,即為什么Promise是可信賴的,更重要的是,為什么在構造魯棒、可維護的軟件時,信任是那么重要。
在JS中,你能在沒有信任的情況下編寫異步代碼嗎?當然,你可以。我們JS開發者已經只用回調異步編程快二十年了。
你對你所建立之上的機制信任到什么程度,才能夠使之可預測和可依賴,一旦你開始質疑,你就會漸漸意識到回調的信任根基并不十分牢固。
Promise是以可信賴語義增強回調的一種模式,因此其行為更合理,更值得信賴。通過反逆轉回調的控制權反轉,我們采用專門用來健全異步的可信賴系統(Promise)來進行控制。
鏈式流(Chain Flow)
我們已經暗示過好多次了,Promise不僅僅是一個單步的this-then-that操作。當然,那是一個構建塊,但是結果表明,我們可以串起多個Promise來代表一系列的異步步驟。
成功的關鍵在于Promise的兩個內在的行為:
- 每次對Promise調用
then(..)
時,都會創建并返回一個新的Promise,我們可以對其進行鏈式操作。 -
then(..)
調用的fulfillment回調(第一個參數)返回的任何值都會自動設置為鏈式Promise(見第一點)的fulfillment。
讓我們首先說明一下是什么意思,之后我們會明白Promise是如何幫助我們創建異步序列控制流的。考慮如下代碼:
var p = Promise.resolve( 21 );
var p2 = p.then( function(v){
console.log( v ); // 21
// fulfill `p2` with value `42`
return v * 2;
} );
// chain off `p2`
p2.then( function(v){
console.log( v ); // 42
} );
通過返回v * 2
(即42
),我們將第一個then(..)
調用生成的promise p2
置成成功狀態,當調用p2
的then(..)
時,它從return v * 2
語句接收fulfillment。當然,p2.then(..)
創建了另一個promise,我們可將它存儲在p3
變量中。
但是必須創建中間變量p2
(或者p3
等)有點惱人。值得慶幸的是,我們可以簡單地將它們串聯起來:
var p = Promise.resolve( 21 );
p
.then( function(v){
console.log( v ); // 21
// fulfill the chained promise with value `42`
return v * 2;
} )
// here's the chained promise
.then( function(v){
console.log( v ); // 42
} );
那么現在第一個then(..)
就是異步序列中的第一步,第二個then(..)
就是第二步。只要我們需要,就可以一直往下擴展。只要通過自動生成的Promise,鏈接到前一個then(..)
上就行了。
但此處似乎少了什么東西。要是我們想讓步驟2等待步驟1作一些異步操作呢?我們采用的是立即的(immediately)return
語句,會立刻將鏈式的promise置為成功狀態。
回想一下,當你傳給它的是一個Promise或者thenable而不是一個最終值時(譯者注:此處指立即值),Promise.resolve(..)
是如何運行的,這是使得一個Promise序列在每一步都具有異步能力的關鍵。Promise.resolve(..)
會直接返回接收的真正Promise,或者拆析(unwrap)接收的thenable的值--并且在拆析(unwrap)thenable時會一直遞歸下去。
如果你從fulfillment(或者rejection)的回調函數中return
一個thenable或者Promise時,也會發生同樣的拆解。
考慮如下:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// create a promise and return it
return new Promise( function(resolve,reject){
// fulfill with value `42`
resolve( v * 2 );
} );
} )
.then( function(v){
console.log( v ); // 42
} );
即使我們將42
包裝到了返回的promise中,它仍然會被拆析(unwrap)并最終作為鏈式promise的解析項,以便第二個then(..)
仍然接收到42
。如果我們將異步引入到那個包裝promise中,一切照舊:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// create a promise to return
return new Promise( function(resolve,reject){
// introduce asynchrony!
setTimeout( function(){
// fulfill with value `42`
resolve( v * 2 );
}, 100 );
} );
} )
.then( function(v){
// runs after the 100ms delay in the previous step
console.log( v ); // 42
} );
好強大!現在我們可以按需構建任意多的異步步驟,并且每一步都可以按需推遲(或不推遲)下一步的執行。
當然,這些例子中步驟之間傳遞的值是可選的。如果你不返回一個顯式的值,則會假定有個隱式的undefined
,并且promise還是以同樣的方式串起來。每個Promise的解析項只是進行到下一步的信號。
為了更進一步說明鏈式,讓我們創建一個通用utility,用來生成延時Promise,使之能夠多步復用:
function delay(time) {
return new Promise( function(resolve,reject){
setTimeout( resolve, time );
} );
}
delay( 100 ) // step 1
.then( function STEP2(){
console.log( "step 2 (after 100ms)" );
return delay( 200 );
} )
.then( function STEP3(){
console.log( "step 3 (after another 200ms)" );
} )
.then( function STEP4(){
console.log( "step 4 (next Job)" );
return delay( 50 );
} )
.then( function STEP5(){
console.log( "step 5 (after another 50ms)" );
} )
...
調用delay(200)
會創建一個200ms后fulfill的promise,然后從第一個then(..)
的fulfillment回調中返回,這會讓第二個then(..)
的promise等那個200ms的promise。
注意: 如上所述,在交替過程中有兩個promise:200ms延時的promise和來自第二個then(..)
所鏈的promise(譯者注:指第一個then(..)
生成的promise)。但是你從心理上覺得將這兩個promise整合起來會更容易理解,Promise機制自動為你整合狀態。從那個角度講,你可以認為return delay(200)
創建了一個promise并替代了早先返回的鏈式promise。
然而,老實說,沒有信息傳遞的序列延時并不是一個特別有用的Promise流控制的例子。讓我們看一個更有實際意義的場景。
讓我們考慮Ajax請求,而不是定時器:
// assume an `ajax( {url}, {callback} )` utility
// Promise-aware ajax
function request(url) {
return new Promise( function(resolve,reject){
// the `ajax(..)` callback should be our
// promise's `resolve(..)` function
ajax( url, resolve );
} );
}
我們首先定義了一個request(..)
utility,用來構建一個promise代表ajax(..)
調用的完成:
request( "http://some.url.1/" )
.then( function(response1){
return request( "http://some.url.2/?v=" + response1 );
} )
.then( function(response2){
console.log( response2 );
} );
注意: 開發者常遇到的一種情形是,他們想采用一些本身不支持Promise(Promise-enabled)的第三方utility(比如此處的ajax(..)
,它需要一個回調函數)來實現類Promise(Promise-aware)的異步流控制。盡管原生的ES6 Promise
機制無法自動為我們提供這種模式,但是所有的Promise庫會提供。通常稱這個過程為“提升(lifting)”或者“promise化(promisifying)” 或者其它變體。我們之后會討論這一技術。
我們通過第一個URL調用request(..)
(能返回Promise(Promise-returning))來隱式創建鏈的第一步,然后用第一個then(..)
鏈接到返回的promise上。
一旦response1
返回,我們用那個值來構建第二個URL,作第二次request(..)
調用。第二個request(..)
返回promise后,異步流中的第三步等待Ajax調用完成。最終,一旦返回值,立即打印response2
。
我們構建的Promise鏈不僅僅是一個表示多步異步序列的流控制,還充當了步與步之間傳遞信息的通道。
要是Promise鏈中的某一步出錯了呢?錯誤/異常是基于單個Promise的(譯者注:指異常發生在生成Promise的每個鏈中),這意味著可以在鏈中的任一點捕獲這個錯誤,并且那個捕獲在那時充當了“重設”鏈回到正常操作的角色:
// step 1:
request( "http://some.url.1/" )
// step 2:
.then( function(response1){
foo.bar(); // undefined, error!
// never gets here
return request( "http://some.url.2/?v=" + response1 );
} )
// step 3:
.then(
function fulfilled(response2){
// never gets here
},
// rejection handler to catch the error
function rejected(err){
console.log( err ); // `TypeError` from `foo.bar()` error
return 42;
}
)
// step 4:
.then( function(msg){
console.log( msg ); // 42
} );
當step2中發生錯誤時,step3中的rejection回調捕獲到該異常。如果rejection函數中有返回值(此處代碼中為42
),則為step4將promise置為成功狀態,以便讓鏈回到fulfillment狀態。
注意: 正如我們早前討論的一樣,當從一個fulfillment函數中返回一個promise時,它會被拆析(unwrap)并且推遲下一步。這同樣也適用于rejection函數,即如果step3返回一個promise而不是return 42
,則那個promise會推遲step4。一個then(..)
中的fulfillment和rejection回調中的異常都會將下一個(鏈式)promise立即置為失敗狀態。
如果你在一個promise上調用then(..)
,并且你只傳了一個fulfillment回調,則會替換為一個假定的rejection處理函數:
var p = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = p.then(
function fulfilled(){
// never gets here
}
// assumed rejection handler, if omitted or
// any other non-function value passed
// function(err) {
// throw err;
// }
);
如你所見,假定的rejection處理函數只是簡單地重新拋出錯誤,最終強制p2
(鏈式的promise)以同樣的錯誤原因reject。本質上,這允許錯誤沿著Promise鏈繼續傳播,直至遇到一個顯式定義的rejection處理函數。
注意: 關于錯誤處理,稍后我們會討論更多細節,因為有些其它微妙的細節需要關注。
如果沒向then(..)
中傳遞一個有效的fulfillment處理函數,則同樣也會替換為一個默認的處理函數:
var p = Promise.resolve( 42 );
p.then(
// assumed fulfillment handler, if omitted or
// any other non-function value passed
// function(v) {
// return v;
// }
null,
function rejected(err){
// never gets here
}
);
如你所見,默認的fulfillment處理函數只是簡單地將接收到的值傳遞到下一步(Promise)。
注意: then(null,function(err){ .. })
模式--只處理rejection(如果有的話)而讓fulfillment通過--有一個快捷的API:catch(function(err){ .. })
。我們將在下一節中詳盡地討論catch(..)
。
讓我們簡單回顧下Promise支持的鏈式流控制的固有行為:
- 對一個Promise調用
then(..)
會自動生成一個新的Promise并返回。 - 在fulfillment/rejection處理函數內部,如果返回一個值或者拋出一個異常,則新返回的(鏈式)Promise會相應地得到解析。
- 如果fulfillment或者rejection處理函數返回一個Promise,則該Promise會被拆析(unwrap),它的解析結果會成為當前
then(..)
返回的鏈式Promise的解析結果。
盡管鏈式流控制很有用,但最確切地說,這是Promise組成(組合)方式的一個附加好處,而不是主要目的。正如我們多次討論過的,Promise將異步標準化并且封裝了與時間無關的值狀態,那才是我們得以用這種有用的方式將它們鏈在一起的原因。
當然,在處理第二章我們提出的回調混亂問題時,鏈式的序列化表示(this-then-this-then-this...)是個巨大改善。但是仍然有相當數量的樣板(then(..)
和function(){ .. }
)需要費力的讀完。在下一章,我們將會見到一個明顯更好的、采用生成器的序列化流控制表示。
術語:解析,成功,失敗(Terminology: Resolve, Fulfill, and Reject)
在更深入學習Promise前,我們需要搞清楚一些有點混淆的術語,“resolve”,“fulfill”和“reject”。首先考慮Promise(..)
構造函數:
var p = new Promise( function(X,Y){
// X() for fulfillment
// Y() for rejection
} );
如你所見,兩個回調函數(此處標為X
和Y
)。第一個通常用于將Promise標為fulfilled狀態,第二個總是將Promise標為rejected狀態。但“通常”是指什么?準確地命名這些參數(譯者注:指X
和Y
)意味著什么?
最終,只是你的用戶代碼對引擎有用,標識符名稱對引擎而言沒任何意義。因此從技術上來說,標識符名稱并不重要,foo(..)
和bar(..)
同樣可以。但是你所用的詞不但影響你如何思考這段代碼,而且也影響團隊中的其他開發人員。對精心編排的異步代碼的錯誤思考比意大利面條式的回調更糟。
因此,如何稱呼它們真的有點重要。
第二個參數很容易確定。幾乎所有的文章采用reject(..)
作為它的名字,因為這就是它(也是唯一!)做的事,是個非常好的命名選擇。我強烈建議你總是使用reject(..)
。
但是關于第一個參數,就有點模糊不清了,在Promise文獻中通常稱為resolve(..)
,那個詞很明顯和“resolution”相關,即在所有文獻中(包括這本書)用來描述給一個Promise設置最終值/狀態。我們已經好幾次使用"resolve the Promise"來表示fulfill或者reject一個Promise。
但是,如果這個參數看起來似乎是專門用于fulfill Promise,為什么我們不叫它fulfill(..)
,而叫它resolve(..)
更準確呢?為了回答這個問題,我們也來看一下兩個Promise
API:
var fulfilledPr = Promise.resolve( 42 );
var rejectedPr = Promise.reject( "Oops" );
Promise.resolve(..)
創建了一個按所給值解析的Promise。在這個例子中,42
是個正常的,非Promise,非thenable的值。因此,以值42
創建了fulfilled的promise fulfilledPr
。Promise.reject("Oops")
以原因短語"Oops"
創建了一個rejected的promise rejectedPr
。
現在讓我們舉例說明一下,如果顯式地用在可能導致fulfillment或者rejection的上下文中,為什么“resolve”一詞(比如 Promise.resolve(..)
中)很清晰并且確實更準確:
var rejectedTh = {
then: function(resolved,rejected) {
rejected( "Oops" );
}
};
var rejectedPr = Promise.resolve( rejectedTh );
正如本章早些時候討論的那樣,Promise.resolve(..)
會直接返回接收的真實Promise,或者拆析(unwrap)接收的thenable。如果拆析(unwrap)的thenable顯示的是rejected狀態,則Promise.resolve(..)
事實上返回的是同樣的rejected狀態。
因此,Promise.resolve(..)
是個很好的,準確的API方法名稱,因為它實際上既能生成fulfillment,又能生成rejection。
Promise(..)
構造函數的第一個回調參數既會拆析(unwrap)一個thenable(與Promise.resolve(..)
一致),也可拆析(unwrap)一個真正的Promise:
var rejectedPr = new Promise( function(resolve,reject){
// resolve this promise with a rejected promise
resolve( Promise.reject( "Oops" ) );
} );
rejectedPr.then(
function fulfilled(){
// never gets here
},
function rejected(err){
console.log( err ); // "Oops"
}
);
現在應該明白resolve(..)
是Promise(..)
構造函數的第一個回調參數的合適名稱了吧。
警告: 前面提到的reject(..)
并不會像resolve(..)
一樣進行拆析。如果你向reject(..)
中傳入一個Promise/thenable。則該未處理的值將被設為rejection原因短語。隨后的rejection處理函數會接收到你傳入reject(..)
的Promise/thenable,而不是最終拆析的立即值。
現在讓我們的注意力回到then(..)
中的回調。它們應該稱為什么(無論是在文獻中還是在代碼中)?我建議fulfilled(..)
和rejected(..)
:
function fulfilled(msg) {
console.log( msg );
}
function rejected(err) {
console.error( err );
}
p.then(
fulfilled,
rejected
);
then(..)
中的第一個參數,毫無疑問總是fulfillment情況,因此沒有 必要使用二義性術語“resolve”。順便提一下,ES6規范中使用onFulfilled(..)
和onRejected(..)
來標記這兩個回調,因此使用這兩個術語很準確的。