感謝社區中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優惠,并抽取幸運大獎:點擊這里領取
在第二章中,我們定位了在使用回調表達程序異步性和管理并發的兩個主要類別的不足:缺乏順序性和缺乏可靠性。現在我們更親近地理解了問題,是時候將我們的注意力轉向解決它們的模式了。
我們首先想要解決的是 控制倒轉 問題,信任是如此脆弱而且是如此的容易丟失。
回想一下,我們將我們的程序的延續包裝進一個回調函數中,將這個回調交給另一個團體(甚至是潛在的外部代碼),并雙手合十祈禱它會做正確的事情并調用這個回調。
我們這么做是因為我們想說,“這是 稍后 將要發生的事,在當前的步驟完成之后。”
但是如果我們能夠反向倒轉這種 控制倒轉 呢?如果不是將我們程序的延續交給另一個團體,而是希望它返回給我們一個可以知道它何時完成的能力,然后我們的代碼可以決定下一步做什么呢?
這種規范被稱為 Promise。
Promise正在像風暴一樣席卷JS世界,因為開發者和語言規范作者之流拼命地想要在他們的代碼/設計中結束回調地獄的瘋狂。事實上,大多數新被加入JS/DOM平臺的異步API都是建立在Promise之上的。所以深入學習它們可能是個好主意,你不這么認為嗎?
注意: “立即”這個詞將在本章頻繁使用,一般來說它指代一些Promise解析行為。然而,本質上在所有情況下,“立即”意味著就工作隊列行為(參見第一章)而言,不是嚴格同步的 現在 的感覺。
什么是Promise?
當開發者們決定要學習一種新技術或模式的時候,他們的第一步總是“給我看代碼!”。摸著石頭過河對我們來講是十分自然的。
但事實上僅僅考察API丟失了一些抽象過程。Promise是這樣一種工具:它能非常明顯地看出使用者是否理解了它是為什么和關于什么,還是僅僅學習和使用API。
所以在我展示Promise的代碼之前,我想在概念上完整地解釋一下Promise到底是什么。我希望這能更好地指引你探索如何將Promise理論整合到你自己的異步流程中。
帶著這樣的想法,讓我們來看兩種類比,來解釋Promise是什么。
未來的值
想象這樣的場景:我走到快餐店的柜臺前,點了一個起士漢堡。并交了1.47美元的現金。通過點餐和付款,我為得到一個 值(起士漢堡)制造了一個請求。我發起了一個事務。
但是通常來說,起士漢堡不會立即到我手中。收銀員交給一些東西代替我的起士漢堡:一個帶有點餐排隊號的收據。這個點餐號是一個“我欠你”的許諾(Promise),它保證我最終會得到我的起士漢堡。
于是我就拿著我的收據和點餐號。我知道它代表我的 未來的起士漢堡,所以我無需再擔心它——除了挨餓!
在我等待的時候,我可以做其他的事情,比如給我的朋友發微信說,“嘿,一塊兒吃午餐嗎?我要吃起士漢堡”。
我已經在用我的 未來的起士漢堡 進行推理了,即便它還沒有到我手中。我的大腦可以這么做是因為它將點餐號作為起士漢堡的占位符號。這個占位符號實質上使這個值 與時間無關。它是一個 未來的值。
最終,我聽到,“113號!”。于是我愉快地拿著收據走回柜臺前。我把收據遞給收銀員,拿回我的起士漢堡。
換句話說,一旦我的 未來的值 準備好,我就用我的許諾值換回值本身。
但還有另外一種可能的輸出。它們叫我的號,但當我去取起士漢堡時,收銀員遺憾地告訴我,“對不起,看起來我們的起士漢堡賣光了。”把這種場景下顧客有多沮喪放在一邊,我們可以看到 未來的值 的一個重要性質:它們既可以表示成功也可以表示失敗。
每次我點起士漢堡時,我都知道我要么最終得到一個起士漢堡,要么得到起士漢堡賣光的壞消息,并且不得不考慮中午吃點兒別的東西。
注意: 在代碼中,事情沒有這么簡單,因為還隱含著一種點餐號永遠也不會被叫到的情況,這時我們就被擱置在了一種無限等待的未解析狀態。我們待會兒再回頭處理這種情況。
現在和稍后的值
這一切也許聽起來在思維上太過抽象而不能實施在你的代碼中。那么,讓我們更具體一些。
然而,在我們能介紹Promise是如何以這種方式工作之前,我們先看看我們已經明白的代碼——回調!——是如何處理這些 未來值 的。
在你寫代碼來推導一個值時,比如在一個number
上進行數學操作,不論你是否理解,對于這個值你已經假設了某些非常基礎的事實——這個值已經是一個實在的 現在 值:
var x, y = 2;
console.log( x + y ); // NaN <-- 因為`x`還沒有被賦值
x + y
操作假定x
和y
都已經被設定好了。用我們一會將要闡述的術語來講,我們假定x
和y
的值已經被 解析(resovle) 了。
期盼+
操作符本身能夠魔法般地檢測并等待x
和y
的值被解析(也就是準備好),然后僅在那之后才進行操作是沒道理的。如果不同的語句 現在 完成而其他的 稍后 完成,這就會在程序中造成混亂,對吧?
如果兩個語句中的一個(或兩者同時)可能還沒有完成,你如何才能推斷它們的關系呢?如果語句2要依賴語句1的完成,那么這里僅有兩種輸出:不是語句1 現在 立即完成而且一切處理正常進行,就是語句1還沒有完成,所以語句2將會失敗。
如果這些東西聽起來很像第一章的內容,很好!
回到我們的x + y
的數學操作。想象有一種方法可以說,“將x
和y
相加,但如果它們中任意一個還沒有被設置,就等到它們都被設置。盡快將它們相加。”
你的大腦也許剛剛跳進回調。好吧,那么...
function add(getX,getY,cb) {
var x, y;
getX( function(xVal){
x = xVal;
// 兩者都準備好了?
if (y != undefined) {
cb( x + y ); // 發送加法的結果
}
} );
getY( function(yVal){
y = yVal;
// 兩者都準備好了?
if (x != undefined) {
cb( x + y ); // 發送加法的結果
}
} );
}
// `fetchX()`和`fetchY()`是同步或異步的函數
add( fetchX, fetchY, function(sum){
console.log( sum ); // 很簡單吧?
} );
花點兒時間來感受一下這段代碼的美妙(或者丑陋),我耐心地等你。
雖然丑陋是無法否認的,但是關于這種異步模式有一些非常重要的事情需要注意。
在這段代碼中,我們將x
和y
作為未來的值對待,我們將add(..)
操作表達為:(從外部看來)它并不關心x
或y
或它們兩者現在是否可用。換句話所,它泛化了 現在 和 稍后,如此我們可以信賴add(..)
操作的一個可預測的結果。
通過使用一個臨時一致的add(..)
——它跨越 現在 和 稍后 的行為是相同的——異步代碼的推理變得容易的多了。
更直白地說:為了一致地處理 現在 和 稍后,我們將它們都作為 稍后:所有的操作都變成異步的。
當然,這種粗略的基于回調的方法留下了許多提升的空間。為了理解在不用關心 未來的值 在時間上什么時候變得可用的情況下推理它而帶來的好處,這僅僅是邁出的一小步。
Promise值
我們絕對會在本章的后面深入更多關于Promise的細節——所以如果這讓你犯糊涂,不要擔心——但讓我們先簡單地看一下我們如何通過Promise
來表達x + y
的例子:
function add(xPromise,yPromise) {
// `Promise.all([ .. ])`接收一個Promise的數組,
// 并返回一個等待它們全部完成的新Promise
return Promise.all( [xPromise, yPromise] )
// 當這個Promise被解析后,我們拿起收到的`X`和`Y`的值,并把它們相加
.then( function(values){
// `values`是一個從先前被解析的Promise那里收到的消息數組
return values[0] + values[1];
} );
}
// `fetchX()`和`fetchY()`分別為它們的值返回一個Promise,
// 這些值可能在 *現在* 或 *稍后* 準備好
add( fetchX(), fetchY() )
// 為了將兩個數字相加,我們得到一個Promise。
// 現在我們鏈式地調用`then(..)`來等待返回的Promise被解析
.then( function(sum){
console.log( sum ); // 這容易多了!
} );
在這個代碼段中有兩層Promise。
fetchX()
和fetchY()
被直接調用,它們的返回值(promise!)被傳入add(..)
。這些promise表示的值將在 現在 或 稍后 準備好,但是每個promise都將行為泛化為與時間無關。我們以一種時間無關的方式來推理X
和Y
的值。它們是 未來值。
第二層是由add(..)
創建(通過Promise.all([ .. ])
)并返回的promise,我們通過調用then(..)
來等待它。當add(..)
操作完成后,我們的sum
未來值 就準備好并可以打印了。我們將等待X
和Y
的 未來值 的邏輯隱藏在add(..)
內部。
注意: 在add(..)
內部。Promise.all([ .. ])
調用創建了一個promise(它在等待promiseX
和promiseY
被解析)。鏈式調用.then(..)
創建了另一個promise,它的return values[0] + values[1]
這一行會被立即解析(使用加法的結果)。這樣,我們鏈接在add(..)
調用末尾的then(..)
調用——在代碼段最后——實際上是在第二個被返回的promise上進行操作,而非被Promise.all([ .. ])
創建的第一個promise。另外,雖然我們沒有在這第二個then(..)
的末尾鏈接任何操作,它也已經創建了另一個promise,我們可以選擇監聽/使用它。這類Promise鏈的細節將會在本章后面進行講解。
就像點一個起士漢堡,Promise的解析可能是一個拒絕(rejection)而非完成(fulfillment)。不同的是,被完成的Promise的值總是程序化的,而一個拒絕值——通常被稱為“拒絕理由”——既可以被程序邏輯設置,也可以被運行時異常隱含地設置。
使用Promise,then(..)
調用實際上可以接受兩個函數,第一個用作完成(正如剛才所示),而第二個用作拒絕:
add( fetchX(), fetchY() )
.then(
// 完成處理器
function(sum) {
console.log( sum );
},
// 拒絕處理器
function(err) {
console.error( err ); // 倒霉!
}
);
如果在取得X
或Y
時出現了錯誤,或在加法操作時某些事情不知怎地失敗了,add(..)
返回的promise就被拒絕了,傳入then(..)
的第二個錯誤處理回調函數會從promise那里收到拒絕的值。
因為Promise包裝了時間相關的狀態——等待當前值的完成或拒絕——從外部看來,Promise本身是時間無關的,如此Promise就可以用可預測的方式組合,而不用關心時間或底層的結果。
另外,一旦Promise被解析,它就永遠保持那個狀態——它在那個時刻變成了一個 不可變的值——而且可以根據需要 被監聽 任意多次。
注意: 因為Promise一旦被解析就是外部不可變的,所以現在將這個值傳遞給任何其他團體都是安全的,而且我們知道它不會被意外或惡意地被修改。這在許多團體監聽同一個Promise的解析時特別有用。一個團體去影響另一個團體對Promise解析的監聽能力是不可能的。不可變性聽起來是一個學院派話題,但它實際上是Promise設計中最基礎且最重要的方面之一,因此不能將它隨意地跳過。
這是用于理解Promise的最強大且最重要的概念之一。通過大量的工作,你可以僅僅使用丑陋的回調組合來創建相同的效果,但這真的不是一個高效的策略,特別是你不得不一遍一遍地重復它。
Promise是一種用來包裝與組合 未來值,并且可以很容易復用的機制。
完成事件
正如我們剛才看到的,一個獨立的Promise作為一個 未來值 動作。但還有另外一種方式考慮Promise的解析:在一個異步任務的兩個或以上步驟中,作為一種流程控制機制——俗稱“這個然后那個”。
讓我們想象調用foo(..)
來執行某個任務。我們對它的細節一無所知,我們也不關心。它可能會立即完成任務,也可能會花一段時間完成。
我們僅僅想簡單地知道foo(..)
什么時候完成,以便于我們可以移動到下一個任務。換句話說,我們想要一種方法被告知foo(..)
的完成,以便于我們可以 繼續。
在典型的JavaScript風格中,如果你需要監聽一個通知,你很可能會想到事件(event)。那么我們可以將我們的通知需求重新表述為,監聽由foo(..)
發出的 完成(或 繼續)事件。
注意: 將它稱為一個“完成事件”還是一個“繼續事件”取決于你的角度。你是更關心foo(..)
發生的事情,還是更關心foo(..)
完成 之后 發生的事情?兩種角度都對而且都有用。事件通知告訴我們foo(..)
已經 完成,但是 繼續 到下一個步驟也沒問題。的確,你為了事件通知調用而傳入的回調函數本身,在前面我們稱它為一個 延續。因為 完成事件 更加聚焦于foo(..)
,也就是我們當前注意的東西,所以在這篇文章的其余部分我們稍稍偏向于使用 完成事件。
使用回調,“通知”就是被任務(foo(..)
)調用的我們的回調函數。但是使用Promise,我們將關系扭轉過來,我們希望能夠監聽一個來自于foo(..)
的事件,當我們被通知時,做相應的處理。
首先,考慮一些假想代碼:
foo(x) {
// 開始做一些可能會花一段時間的事情
}
foo( 42 )
on (foo "completion") {
// 現在我們可以做下一步了!
}
on (foo "error") {
// 噢,在`foo(..)`中有某些事情搞錯了
}
我們調用foo(..)
然后我們設置兩個事件監聽器,一個給"completion"
,一個給"error"
——foo(..)
調用的兩種可能的最終結果。實質上,foo(..)
甚至不知道調用它的代碼監聽了這些事件,這構成了一個非常美妙的 關注分離(separation of concerns)。
不幸的是,這樣的代碼將需要JS環境不具備的一些“魔法”(而且顯得有些不切實際)。這里是一種用JS表達它的更自然的方式:
function foo(x) {
// 開始做一些可能會花一段時間的事情
// 制造一個`listener`事件通知能力并返回
return listener;
}
var evt = foo( 42 );
evt.on( "completion", function(){
// 現在我們可以做下一步了!
} );
evt.on( "failure", function(err){
// 噢,在`foo(..)`中有某些事情搞錯了
} );
foo(..)
明確地創建并返回了一個事件監聽能力,調用方代碼接收并在它上面注冊了兩個事件監聽器。
很明顯這反轉了一般的面向回調代碼,而且是有意為之。與將回調傳入foo(..)
相反,它返回一個我們稱之為evt
的事件能力,它接收回調。
但如果你回想第二章,回調本身代表著一種 控制反轉。所以反轉回調模式實際上是 反轉的反轉,或者說是一個 控制非反轉——將控制權歸還給我們希望保持它的調用方代碼,
一個重要的好處是,代碼的多個分離部分都可以被賦予事件監聽能力,而且它們都可在foo(..)
完成時被獨立地通知,來執行后續的步驟:
var evt = foo( 42 );
// 讓`bar(..)`監聽`foo(..)`的完成
bar( evt );
// 同時,讓`baz(..)`監聽`foo(..)`的完成
baz( evt );
控制非反轉 導致了更好的 關注分離,也就是bar(..)
和baz(..)
不必卷入foo(..)
是如何被調用的問題。相似地,foo(..)
也不必知道或關心bar(..)
和baz(..)
的存在或它們是否在等待foo(..)
完成的通知。
實質上,這個evt
對象是一個中立的第三方團體,在分離的關注點之間進行交涉。
Promise“事件”
正如你可能已經猜到的,evt
事件監聽能力是一個Promise的類比。
在一個基于Promise的方式中,前面的代碼段將會使foo(..)
創建并返回一個Promise
實例,而且這個promise將會被傳入bar(..)
和baz(..)
。
注意: 我們監聽的Promise解析“事件”并不是嚴格的事件(雖然它們為了某些目的表現得像事件),而且它們也不經常稱為"completion"
或"error"
。相反,我們用then(..)
來注冊一個"then"
事件。或者也許更準確地講,then(..)
注冊了"fulfillment(完成)"
和/或"rejection(拒絕)"
事件,雖然我們在代碼中不會看到這些名詞被明確地使用。
考慮:
function foo(x) {
// 開始做一些可能會花一段時間的事情
// 構建并返回一個promise
return new Promise( function(resolve,reject){
// 最終需要調用`resolve(..)`或`reject(..)`
// 它們是這個promise的解析回調
} );
}
var p = foo( 42 );
bar( p );
baz( p );
注意: 在new Promise( function(..){ .. } )
中展示的模式通常被稱為“揭示構造器(revealing constructor)”。被傳入的函數被立即執行(不會被異步推遲,像then(..)
的回調那樣),而且它被提供了兩個參數,我們叫它們resolve
和reject
。這些是Promise的解析函數。resolve(..)
一般表示完成,而reject(..)
表示拒絕。
你可能猜到了bar(..)
和baz(..)
的內部看起來是什么樣子:
function bar(fooPromise) {
// 監聽`foo(..)`的完成
fooPromise.then(
function(){
// `foo(..)`現在完成了,那么做`bar(..)`的任務
},
function(){
// 噢,在`foo(..)`中有某些事情搞錯了
}
);
}
// `baz(..)`同上
Promise解析沒有必要一定發送消息,就像我們將Promise作為 未來值 考察時那樣。它可以僅僅作為一種流程控制信號,就像前面的代碼中那樣使用。
另一種表達方式是:
function bar() {
// `foo(..)`絕對已經完成了,那么做`bar(..)`的任務
}
function oopsBar() {
// 噢,在`foo(..)`中有某些事情搞錯了,那么`bar(..)`不會運行
}
// `baz()`和`oopsBaz()`同上
var p = foo( 42 );
p.then( bar, oopsBar );
p.then( baz, oopsBaz );
注意: 如果你以前見過基于Promise的代碼,你可能會相信這段代碼的最后兩行應當寫做p.then( .. ).then( .. )
,使用鏈接,而不是p.then(..); p.then(..)
。這將會是兩種完全不同的行為,所以要小心!這種區別現在看起來可能不明顯,但是它們實際上是我們目前還沒有見過的異步模式:分割(splitting)/分叉(forking)。不必擔心!本章后面我們會回到這個話題。
與將p
promise傳入bar(..)
和baz(..)
相反,我們使用promise來控制bar(..)
和baz(..)
何時該運行,如果有這樣的時刻。主要區別在于錯誤處理。
在第一個代碼段的方式中,無論foo(..)
是否成功bar(..)
都會被調用,如果被通知foo(..)
失敗了的話它提供自己的后備邏輯。顯然,baz(..)
也是這樣做的。
在第二個代碼段中,bar(..)
僅在foo(..)
成功后才被調用,否則oopsBar(..)
會被調用。baz(..)
也是。
兩種方式本身都 對。但會有一些情況使一種優于另一種。
在這兩種方式中,從foo(..)
返回的promisep
都被用于控制下一步發生什么。
另外,兩個代碼段都以對同一個promisep
調用兩次then(..)
結束,這展示了先前的觀點,也就是Promise(一旦被解析)會永遠保持相同的解析結果(完成或拒絕),而且可以按需要后續地被監聽任意多次。
無論何時p
被解析,下一步都將總是相同的,包括 現在 和 稍后。
Thenable鴨子類型(Duck Typing)
在Promise的世界中,一個重要的細節是如何確定一個值是否是純粹的Promise。或者更直接地說,一個值會不會像Promise那樣動作?
我們知道Promise是由new Promise(..)
語法構建的,你可能會想p instanceof Promise
將是一個可以接受的檢查。但不幸的是,有幾個理由表明它不是完全夠用。
主要原因是,你可以從其他瀏覽器窗口中收到Promise值(iframe等),其他的瀏覽器窗口會擁有自己的不同于當前窗口/frame的Promise,這種檢查將會在定位Promise實例時失效。
另外,一個庫或框架可能會選擇實現自己的Promise而不是用ES6原生的Promise
實現。事實上,你很可能在根本沒有Promise的老版本瀏覽器中通過一個庫來使用Promise。
當我們在本章稍后討論Promise的解析過程時,為什么識別并同化一個非純種但相似Promise的值仍然很重要會愈發明顯。但目前只需要相信我,它是拼圖中很重要的一塊。
如此,人們決定識別一個Promise(或像Promise一樣動作的某些東西)的方法是定義一種稱為“thenable”的東西,也就是任何擁有then(..)
方法的對象或函數。這種方法假定任何這樣的值都是一個符合Promise的thenable。
根據值的形狀(存在什么屬性)來推測它的“類型”的“類型檢查”有一個一般的名稱,稱為“鴨子類型檢查”——“如果它看起來像一只鴨子,并且叫起來相一致鴨子,那么它一定是一只鴨子”(參見本叢書的 類型與文法)。所以對thenable的鴨子類型檢查可能大致是這樣:
if (
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
) {
// 認為它是一個thenable!
}
else {
// 不是一個thenable
}
暈!先把將這種邏輯在各種地方實現有點丑陋的事實放在一邊不談,這里還有更多更深層的麻煩。
如果你試著用一個偶然擁有then(..)
函數的任意對象/函數來完成一個Promise,但你又沒想把它當做一個Promise/thenable來對待,你的運氣就用光了,因為它會被自動地識別為一個thenable并以特殊的規則來對待(見本章后面的部分)。
如果你不知道一個值上面擁有then(..)
就更是這樣。比如:
var o = { then: function(){} };
// 使`v`用`[[Prototype]]`鏈接到`o`
var v = Object.create( o );
v.someStuff = "cool";
v.otherStuff = "not so cool";
v.hasOwnProperty( "then" ); // false
v
看起來根本不像是一個Promise或thanable。它只是一個擁有一些屬性的直白的對象。你可能只是想要把這個值像其他對象那樣傳遞而已。
但你不知道的是,v
還[[Prototype]]
連接著(見本叢書的 this與對象原型)另一個對象o
,在它上面偶然擁有一個then(..)
。所以thenable鴨子類型檢查將會認為并假定v
是一個thenable。噢。
它甚至不需要直接故意那么做:
Object.prototype.then = function(){};
Array.prototype.then = function(){};
var v1 = { hello: "world" };
var v2 = [ "Hello", "World" ];
v1
和v2
都將被假定為是thenalbe的。你不能控制或預測是否有其他代碼偶然或惡意地將then(..)
加到Object.prototype
,Array.prototype
,或其他任何原生原型上。而且如果這個指定的函數并不將它的任何參數作為回調調用,那么任何用這樣的值被解析的Promise都將無聲地永遠掛起!瘋狂。
聽起來難以置信或不太可能?也許。
要知道,在ES6之前就有幾種廣為人知的非Promise庫在社區中存在了,而且它們已經偶然擁有了稱為then(..)
的方法。這些庫中的一些選擇了重命名它們自己的方法來回避沖突(這很爛!)。另一些則因為它們無法改變來回避沖突,簡單地降級為“不兼容基于Promise的代碼”的不幸狀態。
用來劫持原先非保留的——而且聽起來完全是通用的——then
屬性名稱的標準決議是,沒有值(或它的任何委托),無論是過去,現在,還是將來,可以擁有then(..)
函數,不管是有意的還是偶然的,否則這個值將在Promise系統中被混淆為一個thenable,從而可能產生非常難以追蹤的Bug。
警告: 我不喜歡我們用thenable的鴨子類型來結束對Promise認知的方式。還有其他的選項,比如“branding”或者甚至是“anti-branding”;我們得到的似乎是一個最差勁兒的妥協。但它并不全是悲觀與失望。thenable鴨子類型可以很有用,就像我們馬上要看到的。只是要小心,如果thenable鴨子類型將不是Promise的東西誤認為是Promise,它就可能成為災難。
Promise的信任
我們已經看過了兩個強烈的類比,它們解釋了Promise可以為我們的異步代碼所做的事的不同方面。但如果我們停在這里,我們就可能會錯過一個Promise模式建立的最重要的性質:信任。
隨著 未來值 和 完成事件 的類別在我們探索的代碼模式中的明確展開,有一個問題依然沒有完全明確:Promise是為什么,以及如何被設計為來解決所有我們在第二章“信任問題”一節中提出的 控制倒轉 的信任問題的。但是只要深挖一點兒,我們就可以發現一些重要的保證,來重建第二章中毀掉的對異步代碼的信心!
讓我們從復習僅使用回調的代碼中的信任問題開始。當你傳遞一個回調給一個工具foo(..)
的時候,它可能:
- 調用回調太早
- 調用回調太晚(或根本不調)
- 調用回調太少或太多次
- 沒能傳遞必要的環境/參數
- 吞掉了任何可能發生的錯誤/異常
Promise的性質被有意地設計為給這些顧慮提供有用的,可復用的答案。
調的太早
這種顧慮主要是代碼是否會引入類Zalgo效應,也就是一個任務有時會同步完地成,而有時會異步地完成,這將導致竟合狀態。
Promise被定義為不能受這種顧慮的影響,因為即便是立即完成的Promise(比如 new Promise(function(resolve){ resolve(42); })
)也不可能被同步地 監聽。
也就是說,但你在Promise上調用then(..)
的時候,即便這個Promise已經被解析了,你給then(..)
提供的回調也將 總是 被異步地調用(更多關于這里的內容,參照第一章的"Jobs")。
不必再插入你自己的setTimeout(..,0)
黑科技了。Promise自動地防止了Zalgo效應。
調的太晚
和前一點相似,在resolve(..)
或reject(..)
被Promise創建機制調用時,一個Promise的then(..)
上注冊的監聽回調將自動地被排程。這些被排程好的回調將在下一個異步時刻被可預測地觸發(參照第一章的"Jobs")。
同步監聽是不可能的,所以不可能有一個同步的任務鏈的運行來“推遲”另一個回調的發生。也就是說,當一個Promise被解析時,所有在then(..)
上注冊的回調都將被立即,按順序地,在下一個異步機會時被調用(再一次,參照第一章的"Jobs"),而且沒有任何在這些回調中發生的事情可以影響/推遲其他回調的調用。
舉例來說:
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上的回調之間的相對順序,是不能可靠預測的。
如果兩個promisep1
和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 <-- 不是你可能期望的 B A
我們稍后會更多地講解這個問題,但如你所見,p1
不是被一個立即值所解析的,而是由另一個promisep3
所解析,而p3
本身被一個值"B"
所解析。這種指定的行為將p3
展開 到p1
,但是是異步地,所以在異步工作隊列中p1
的回調位于p2
的回調之后(參照第一章的"Jobs")。
為了回避這樣的微妙的噩夢,你絕不應該依靠任何跨Promise的回調順序/排程。事實上,一個好的實踐方式是在代碼中根本不要讓多個回調的順序成為問題。盡可能回避它。
根本不調回調
這是一個很常見的顧慮。Promise用幾種方式解決它。
首先,沒有任何東西(JS錯誤都不能)可以阻止一個Promise通知你它的解析(如果它被解析了的話)。如果你在一個Promise上同時注冊了完成和拒絕回調,而且這個Promise被解析了,兩個回調中的一個總會被調用。
當然,如果你的回調本身有JS錯誤,你可能不會看到你期望的結果,但是回調事實上已經被調用了。我們待會兒就會講到如何在你的回調中收到關于一個錯誤的通知,因為就算是它們也不會被吞掉。
那如果Promise本身不管怎樣永遠沒有被解析呢?即便是這種狀態Promise也給出了答案,使用一個稱為“競賽(race)”的高級抽象。
// 一個使Promise超時的工具
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}
// 為`foo()`設置一個超時
Promise.race( [
foo(), // 嘗試調用`foo()`
timeoutPromise( 3000 ) // 給它3秒鐘
] )
.then(
function(){
// `foo(..)`及時地完成了!
},
function(err){
// `foo()`不是被拒絕了,就是它沒有及時完成
// 那么可以考察`err`來知道是哪種情況
}
);
這種Promise的超時模式有更多的細節需要考慮,但我們待會兒再回頭討論。
重要的是,我們可以確保一個信號作為foo(..)
的結果,來防止它無限地掛起我們的程序。
調太少或太多次
根據定義,對于被調用的回調來講 一次 是一個合適的次數。“太少”的情況將會是0次,和我們剛剛考察的從不調用是相同的。
“太多”的情況則很容易解釋。Promise被定義為只能被解析一次。如果因為某些原因,Promise的創建代碼試著調用resolve(..)
或reject(..)
許多次,或者試著同時調用它們倆,Promise將僅接受第一次解析,而無聲地忽略后續的嘗試。
因為一個Promise僅能被解析一次,所以任何then(..)
上注冊的(每個)回調將僅僅被調用一次。
當然,如果你把同一個回調注冊多次(比如p.then(f); p.then(f);
),那么它就會被調用注冊的那么多次。響應函數僅被調用一次的保證并不能防止你砸自己的腳。
沒能傳入任何參數/環境
Promise可以擁有最多一個解析值(完成或拒絕)。
如果無論怎樣你沒有用一個值明確地解析它,它的值就是undefined
,就像JS中常見的那樣。但不管是什么值,它總是會被傳入所有被注冊的(并且適當地:完成或拒絕)回調中,不管是 現在 還是將來。
需要意識到的是:如果你使用多個參數調用resolve(..)
或reject(..)
,所有第一個參數之外的后續參數都會被無聲地忽略。雖然這看起來違反了我們剛才描述的保證,但并不確切,因為它構成了一種Promise機制的無效使用方式。其他的API無效使用方式(比如調用resolve(..)
許多次)也都相似地 被保護,所以Promise的行為在這里是一致的(除了有一點點讓人沮喪)。
如果你想傳遞多個值,你必須將它們包裝在另一個單獨的值中,比如一個array
或一個object
。
至于環境,JS中的函數總是保持他們被定義時所在作用域的閉包(見本系列的 作用域與閉包),所以它們理所當然地可以繼續訪問你提供的環境狀態。當然,這對僅使用回調的設計來講也是對的,所以這不能算是Promise帶來的增益——但盡管如此,它依然是我們可以依賴的保證。
吞掉所有錯誤/異常
在基本的感覺上,這是前一點的重述。如果你用一個 理由(也就是錯誤消息)拒絕一個Promise,這個值就會被傳入拒絕回調。
但是這里有一個更重要的事情。如果在Promise的創建過程中的任意一點,或者在監聽它的解析的過程中,一個JS異常錯誤發生的話,比如TypeError
或ReferenceError
,這個異常將會被捕獲,并且強制當前的Promise變為拒絕。
舉例來說:
var p = new Promise( function(resolve,reject){
foo.bar(); // `foo`沒有定義,所以這是一個錯誤!
resolve( 42 ); // 永遠不會跑到這里 :(
} );
p.then(
function fulfilled(){
// 永遠不會跑到這里 :(
},
function rejected(err){
// `err`將是一個來自`foo.bar()`那一行的`TypeError`異常對象
}
);
在foo.bar()
上發生的JS異常變成了一個你可以捕獲并響應的Promise拒絕。
這是一個重要的細節,因為它有效地解決了另一種潛在的Zalgo時刻,也就是錯誤可能會產生一個同步的反應,而沒有錯誤的部分還是異步的。Promise甚至將JS異常都轉化為異步行為,因此極大地降低了發生竟合狀態的可能性。
但是如果Promise完成了,但是在監聽過程中(在一個then(..)
上注冊的回調上)出現了JS異常錯誤會怎樣呢?即便是那些也不會丟失,但你可能會發現處理它們的方式有些令人詫異,除非你深挖一些:
var p = new Promise( function(resolve,reject){
resolve( 42 );
} );
p.then(
function fulfilled(msg){
foo.bar();
console.log( msg ); // 永遠不會跑到這里 :(
},
function rejected(err){
// 也永遠不會跑到這里 :(
}
);
等一下,這看起來foo.bar()
發生的異常確實被吞掉了。不要害怕,它沒有。但更深層次的東西出問題了,也就是我們沒能成功地監聽他。p.then(..)
調用本身返回另一個promise,是 那個 promise將會被TypeError
異常拒絕。
為什么它不能調用我們在這里定義的錯誤處理器呢?表面上看起來是一個符合邏輯的行為。但它會違反Promise一旦被解析就 不可變 的基本原則。p
已經完成為值42
,所以它不能因為在監聽p
的解析時發生了錯誤,而在稍后變成一個拒絕。
除了違反原則,這樣的行為還可能造成破壞,假如說有多個在promisep
上注冊的then(..)
回調,因為有些會被調用而有些不會,而且至于為什么是很明顯的。
可信的Promise?
為了基于Promise模式建立信任,還有最后一個細節需要考察。
無疑你已經注意到了,Promise根本沒有擺脫回調。它們只是改變了回調傳遞的位置。與將一個回調傳入foo(..)
相反,我們從foo(..)
那里拿回 某些東西 (表面上是一個純粹的Promise),然后我們將回調傳入這個 東西。
但為什么這要比僅使用回調的方式更可靠呢?我們如何確信我們拿回來的 某些東西 事實上是一個可信的Promise?這難道不是說我們相信它僅僅因為我們已經相信它了嗎?
一個Promise經常被忽視,但是最重要的細節之一,就是它也為這個問題給出了解決方案。包含在原生的ES6Promise
實現中,它就是Promise.resolve(..)
。
如果你傳遞一個立即的,非Promise的,非thenable的值給Promise.resolve(..)
,你會得到一個用這個值完成的promise。換句話說,下面兩個promisep1
和p2
的行為基本上完全相同:
var p1 = new Promise( function(resolve,reject){
resolve( 42 );
} );
var p2 = Promise.resolve( 42 );
但如果你傳遞一個純粹的Promise給Promise.resolve(..)
,你會得到這個完全相同的promise:
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true
更重要的是,如果你傳遞一個非Promise的thenable值給Promise.resolve(..)
,它會試著將這個值展開,而且直到抽出一個最終具體的非Promise值之前,展開操作將會一直繼續下去。
還記得我們先前討論的thenable嗎?
考慮這段代碼:
var p = {
then: function(cb) {
cb( 42 );
}
};
// 這工作起來沒問題,但要靠運氣
p
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// 永遠不會跑到這里
}
);
這個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){
// 噢,這里本不該運行
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){
// 永遠不會跑到這里
}
);
Promise.resolve(..)
會接受任何thenable,而且將它展開直至非thenable值。但你會從Promise.resolve(..)
那里得到一個真正的,純粹的Promise,一個你可以信任的東西。如果你傳入的東西已經是一個純粹的Promise了,那么你會單純地將它拿回來,所以通過Promise.resolve(..)
過濾來得到信任沒有任何壞處。
那么我們假定,我們在調用一個foo(..)
工具,而且不能確定我們能相信它的返回值是一個行為規范的Promise,但我們知道它至少是一個thenable。Promise.resolve(..)
將會給我們一個可靠的Promise包裝器來進行鏈式調用:
// 不要只是這么做:
foo( 42 )
.then( function(v){
console.log( v );
} );
// 相反,這樣做:
Promise.resolve( foo( 42 ) )
.then( function(v){
console.log( v );
} );
注意: 將任意函數的返回值(thenable或不是thenable)包裝在Promise.resolve(..)
中的另一個好的副作用是,它可以很容易地將函數調用泛化為一個行為規范的異步任務。如果foo(42)
有時返回一個立即值,而其他時候返回一個Promise,Promise.resolve(foo(42))
,將確保它總是返回Promise。并且使代碼成為回避Zalgo效應的更好的代碼。
信任建立了
希望前面的討論使你現在完全理解了Promise是可靠的,而且更為重要的是,為什么信任對于建造強壯,可維護的軟件來說是如此關鍵。
沒有信任,你能用JS編寫異步代碼嗎?你當然能。我們JS開發者在除了回調以外沒有任何東西的情況下,寫了將近20年的異步代碼了。
但是一旦你開始質疑你到底能夠以多大的程度相信你的底層機制,它實際上多么可預見,多么可靠,你就會開始理解回調的信任基礎多么的搖搖欲墜。
Promise是一個用可靠語義來增強回調的模式,所以它的行為更合理更可靠。通過將回調的 控制倒轉 反置過來,我們將控制交給一個可靠的系統(Promise),它是為了將你的異步處理進行清晰的表達而特意設計的。
鏈式流程
我們已經被暗示過幾次,但Promise不僅是是一個單步的 這個然后那個 操作機制。當然,那是構建塊兒,但事實證明我們可以將多個Promise串聯在一起來表達一系列的異步步驟。
使這一切能夠工作的關鍵,是Promise的兩個固有行為:
- 每次你在一個Promise上調用
then(..)
的時候,它都創建并返回一個新的Promise,我們可以在它上面進行 鏈接。 - 無論你從
then(..)
調用的完成回調中(第一個參數)返回什么值,它都做為被鏈接的Promise的完成。
我們首先來說明一下這是什么意思,然后我們將會延伸出它是如何幫助我們創建異步順序的控制流程的。考慮下面的代碼:
var p = Promise.resolve( 21 );
var p2 = p.then( function(v){
console.log( v ); // 21
// 使用值`42`完成`p2`
return v * 2;
} );
// 在`p2`后鏈接
p2.then( function(v){
console.log( v ); // 42
} );
通過返回v * 2
(也就是42
),我們完成了由第一個then(..)
調用創建并返回的p2
promise。當p2
的then(..)
調用運行時,它從return v * 2
語句那里收到完成信號。當然,p2.then(..)
還會創建另一個promise,我們將它存儲在變量p3
中。
但是不得不創建臨時變量p2
(或p3
等)有點兒惱人。幸運的是,我們可以簡單地將這些鏈接在一起:
var p = Promise.resolve( 21 );
p
.then( function(v){
console.log( v ); // 21
// 使用值`42`完成被鏈接的promise
return v * 2;
} )
// 這里是被鏈接的promise
.then( function(v){
console.log( v ); // 42
} );
那么現在第一個then(..)
是異步序列的第一步,而第二個then(..)
就是第二步。它可以根據你的需要延伸至任意長。只要持續不斷地用每個自動創建的Promise在前一個then(..)
末尾進行連接即可。
但是這里錯過了某些東西。要是我們想讓第2步等待第1步去做一些異步的事情呢?我們使用的是一個立即的return
語句,它立即完成了鏈接中的promise。
使Promise序列在每一步上都是真正異步的關鍵,需要回憶一下當你向Promise.resolve(..)
傳遞一個Promise或thenable而非一個最終值時它如何執行。Promise.resolve(..)
會直接返回收到的純粹Promise,或者它會展開收到的thenable的值——并且它會遞歸地持續展開thenable。
如果你從完成(或拒絕)處理器中返回一個thenable或Promise,同樣的展開操作也會發生。考慮這段代碼:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 創建一個promise并返回它
return new Promise( function(resolve,reject){
// 使用值`42`完成
resolve( v * 2 );
} );
} )
.then( function(v){
console.log( v ); // 42
} );
即便我們把42
包裝在一個我們返回的promise中,它依然會被展開并作為下一個被鏈接的promise的解析,如此第二個then(..)
仍然收到42
。如果我們在這個包裝promise中引入異步,一切還是會同樣正常的工作:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 創建一個promise并返回
return new Promise( function(resolve,reject){
// 引入異步!
setTimeout( function(){
// 使用值`42`完成
resolve( v * 2 );
}, 100 );
} );
} )
.then( function(v){
// 在上一步中的100毫秒延遲之后運行
console.log( v ); // 42
} );
這真是不可思議的強大!現在我們可以構建一個序列,它可以有我們想要的任意多的步驟,而且每一步都可以按照需要來推遲下一步(或者不推遲)。
當然,在這些例子中一步一步向下傳遞的值是可選的。如果你沒有返回一個明確的值,那么它假定一個隱含的undefined
,而且promise依然會以同樣的方式鏈接在一起。如此,每個Promise的解析只不過是進行至下一步的信號。
為了演示更長的鏈接,讓我們把推遲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)
創建了一個將在200毫秒內完成的promise,然后我們在第一個then(..)
的完成回調中返回它,這將使第二個then(..)
的promise等待這個200毫秒的promise。
注意: 正如剛才描述的,技術上講在這個交替中有兩個promise:一個200毫秒延遲的promise,和一個被第二個then(..)
鏈接的promise。但你可能會發現將這兩個promise組合在一起更容易思考,因為Promise機制幫你把它們的狀態自動地混合到了一起。從這個角度講,你可以認為return delay(200)
創建了一個promise來取代早前一個返回的被鏈接的promise。
老實說,沒有任何消息進行傳遞的一系列延遲作為Promise流程控制的例子不是很有用。讓我們來看一個更加實在的場景:
與計時器不同,讓我們考慮發起Ajax請求:
// 假定一個`ajax( {url}, {callback} )`工具
// 帶有Promise的ajax
function request(url) {
return new Promise( function(resolve,reject){
// `ajax(..)`的回調應當是我們的promise的`resolve(..)`函數
ajax( url, resolve );
} );
}
我們首先定義一個request(..)
工具,它構建一個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的工具(就像這里的ajax(..)
,它期待一個回調)進行Promise式的異步流程控制。雖然ES6原生的Promise
機制不會自動幫我們解決這種模式,但是在實踐中所有的Promise庫會幫我們這么做。它們通常稱這種處理為“提升(lifting)”或“promise化”或其他的什么名詞。我們稍后再回頭討論這種技術。
使用返回Promise的request(..)
,通過用第一個URL調用它我們在鏈條中隱式地創建了第一步,然后我們用第一個then(..)
在返回的promise末尾進行連接。
一旦response1
返回,我們用它的值來構建第二個URL,并且發起第二個request(..)
調用。這第二個promise
是return
的,所以我們的異步流程控制的第三步將會等待這個Ajax調用完成。最終,一旦response2
返回,我們就打印它。
我們構建的Promise鏈不僅是一個表達多步驟異步序列的流程控制,它還扮演者將消息從一步傳遞到下一步的消息管道。
要是Promise鏈中的某一步出錯了會怎樣呢?一個錯誤/異常是基于每個Promise的,意味著在鏈條的任意一點捕獲這些錯誤是可能的,而且這些捕獲操作在那一點上將鏈條“重置”,使它回到正常的操作上來:
// 步驟 1:
request( "http://some.url.1/" )
// 步驟 2:
.then( function(response1){
foo.bar(); // 沒有定義,錯誤!
// 永遠不會跑到這里
return request( "http://some.url.2/?v=" + response1 );
} )
// 步驟 3:
.then(
function fulfilled(response2){
// 永遠不會跑到這里
},
// 拒絕處理器捕捉錯誤
function rejected(err){
console.log( err ); // 來自 `foo.bar()` 的 `TypeError` 錯誤
return 42;
}
)
// 步驟 4:
.then( function(msg){
console.log( msg ); // 42
} );
當錯誤在第2步中發生時,第3步的拒絕處理器將它捕獲。拒絕處理器的返回值(在這個代碼段里是42
),如果有的話,將會完成下一步(第4步)的promise,如此整個鏈條又回到完成的狀態。
注意: 就像我們剛才討論過的,當我們從一個完成處理器中返回一個promise時,它會被展開并有可能推遲下一步。這對從拒絕處理器中返回的promise也是成立的,這樣如果我們在第3步返回一個promise而不是return 42
,那么這個promise就可能會推遲第4步。不管是在then(..)
的完成還是拒絕處理器中,一個被拋出的異常都將導致下一個(鏈接著的)promise立即用這個異常拒絕。
如果你在一個promise上調用then(..)
,而且你只向它傳遞了一個完成處理器,一個假定的拒絕處理器會取而代之:
var p = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = p.then(
function fulfilled(){
// 永遠不會跑到這里
}
// 如果忽略或者傳入任何非函數的值,
// 會有假定有一個這樣的拒絕處理器
// function(err) {
// throw err;
// }
);
如你所見,這個假定的拒絕處理器僅僅簡單地重新拋出錯誤,它最終強制p2
(鏈接著的promise)用同樣的錯誤進行拒絕。實質上,它允許錯誤持續地在Promise鏈上傳播,直到遇到一個明確定義的拒絕處理器。
注意: 稍后我們會講到更多關于使用Promise進行錯誤處理的細節,因為會有更多微妙的細節需要關心。
如果沒有一個恰當的合法的函數作為then(..)
的完成處理器參數,也會有一個默認的處理器取而代之:
var p = Promise.resolve( 42 );
p.then(
// 如果忽略或者傳入任何非函數的值,
// 會有假定有一個這樣的完成處理器
// function(v) {
// return v;
// }
null,
function rejected(err){
// 永遠不會跑到這里
}
);
如你所見,默認的完成處理器簡單地將它收到的任何值傳遞給下一步(Promise)。
注意: then(null,function(err){ .. })
這種模式——僅處理拒絕(如果發生的話)但讓成功通過——有一個縮寫的API:catch(function(err){ .. })
。我們會在下一節中更全面地涵蓋catch(..)
。
然我們簡要地復習一下使鏈式流程控制成為可能的Promise固有行為:
- 在一個Promise上的
then(..)
調用會自動生成一個新的Promise并返回。 - 在完成/拒絕處理器內部,如果你返回一個值或拋出一個異常,新返回的Promise(可以被鏈接的)將會相應地被解析。
- 如果完成或拒絕處理器返回一個Promise,它會被展開,所以無論它被解析為什么值,這個值都將變成從當前的
then(..)
返回的被鏈接的Promise的解析。
雖然鏈式流程控制很有用,但是將它認為是Promise的組合方式的副作用可能最準確,而不是它的主要意圖。正如我們已經詳細討論過許多次的,Promise泛化了異步處理并且包裝了與時間相關的值和狀態,這才是讓我們以這種有用的方式將它們鏈接在一起的原因。
當然,相對于我們在第二章中看到的一堆混亂的回調,這種鏈條的順序表達是一個巨大的改進。但是仍然要蹚過相當多的模板代碼(then(..)
and function(){ .. }
)。在下一章中,我們將看到一種極大美化順序流程控制的表達模式,生成器(generators)。
術語: Resolve(解析),Fulfill(完成),和Reject(拒絕)
在你更多深入地學習Promise之前,在“解析(resolve)”,“完成(fulfill)”,和“拒絕(reject)”這些名詞之間還有一些我們需要辨明的小困惑。首先讓我們考慮一下Promise(..)
構造器:
var p = new Promise( function(X,Y){
// X() 給 fulfillment(完成)
// Y() 給 rejection(拒絕)
} );
如你所見,有兩個回調(標識為X
和Y
)被提供了。第一個 通常 用于表示Promise完成了,而第二個 總是 表示Promise拒絕了。但“通常”是什么意思?它對這些參數的正確命名暗示著什么呢?
最終,這只是你的用戶代碼,和將被引擎翻譯為沒有任何含義的東西的標識符,所以在 技術上 它無緊要;foo(..)
和bar(..)
在功能性上是相等的。但是你用的詞不僅會影響你如何考慮這段代碼,還會影響你所在團隊的其他開發者如何考慮它。將精心策劃的異步代碼錯誤地考慮,幾乎可以說要比面條一般的回調還要差勁兒。
所以,某種意義上你如何稱呼它們很關鍵。
第二個參數很容易決定。幾乎所有的文獻都使用reject(..)
做為它的名稱,應為這正是它(唯一!)要做的,對于命名來說這是一個很好的選擇。我也強烈推薦你一直使用reject(..)
。
但是關于第一個參數還是有些帶有歧義,它在許多關于Promise的文獻中常被標識為resolve(..)
。這個詞明顯地是與“resolution(解析)”有關,它在所有的文獻中(包括本書)廣泛用于描述給Promise設定一個最終的值/狀態。我們已經使用“解析Promise(resolve the Promise)”許多次來意味Promise的完成(fulfilling)或拒絕(rejecting)。
但是如果這個參數看起來被用于特指Promise的完成,為什么我們不更準確地叫它fulfill(..)
,而是用resolve(..)
呢?要回答這個問題,讓我們看一下Promise
的兩個API方法:
var fulfilledPr = Promise.resolve( 42 );
var rejectedPr = Promise.reject( "Oops" );
Promise.resolve(..)
創建了一個Promise,它被解析為它被給予的值。在這個例子中,42
是一個一般的,非Promise,非thenable的值,所以完成的promisefulfilledPr
是為值42
創建的。Promise.reject("Oops")
為了原因"Oops"
創建的拒絕的promiserejectedPr
。
現在讓我們來解釋為什么如果“resolve”這個詞(正如Promise.resolve(..)
里的)被明確用于一個既可能完成也可能拒絕的環境時,它沒有歧義,反而更加準確:
var rejectedTh = {
then: function(resolved,rejected) {
rejected( "Oops" );
}
};
var rejectedPr = Promise.resolve( rejectedTh );
就像我們在本章前面討論的,Promise.resolve(..)
將會直接返回收到的純粹的Promise,或者將收到的thenable展開。如果展開這個thenable之后是一個拒絕狀態,那么從Promise.resolve(..)
返回的Promise事實上是相同的拒絕狀態。
所以對于這個API方法來說,Promise.resolve(..)
是一個好的,準確的名稱,因為它實際上既可以得到完成的結果,也可以得到拒絕的結果。
Promise(..)
構造器的第一個回調參數既可以展開一個thenable(與Promise.resolve(..)
相同),也可以展開一個Promise:
var rejectedPr = new Promise( function(resolve,reject){
// 用一個被拒絕的promise來解析這個promise
resolve( Promise.reject( "Oops" ) );
} );
rejectedPr.then(
function fulfilled(){
// 永遠不會跑到這里
},
function rejected(err){
console.log( err ); // "Oops"
}
);
現在應當清楚了,對于Promise(..)
構造器的第一個參數來說resolve(..)
是一個合適的名稱。
警告: 前面提到的reject(..)
不會 像resolve(..)
那樣進行展開。如果你向reject(..)
傳遞一個Promise/thenable值,這個沒有被碰過的值將作為拒絕的理由。一個后續的拒絕處理器將會受到你傳遞給reject(..)
的實際的Promise/thenable,而不是它底層的立即值。
現在讓我們將注意力轉向提供給then(..)
的回調。它們應當叫什么(在文獻和代碼中)?我的建議是fulfilled(..)
和rejected(..)
:
function fulfilled(msg) {
console.log( msg );
}
function rejected(err) {
console.error( err );
}
p.then(
fulfilled,
rejected
);
對于then(..)
的第一個參數的情況,它沒有歧義地總是完成狀態,所以沒有必要使用帶有雙重意義的“resolve”術語。另一方面,ES6語言規范中使用onFulfilled(..)
和onRejected(..)
來標識這兩個回調,所以它們是準確的術語。