# 第三章: Promises
在第二章中,我們定位了在使用回調表達程序異步性和管理并發的兩個主要類別的不足:缺乏順序性和缺乏可靠性。現在我們更親近地理解了問題,是時候將我們的注意力轉向解決它們的模式了。
我們首先想要解決的是?*控制倒轉*?問題,信任是如此脆弱而且是如此的容易丟失。
回想一下,我們將我們的程序的延續包裝進一個回調函數中,將這個回調交給另一個團體(甚至是潛在的外部代碼),并雙手合十祈禱它會做正確的事情并調用這個回調。
我們這么做是因為我們想說,“這是?*稍后*?將要發生的事,在當前的步驟完成之后。”
但是如果我們能夠反向倒轉這種?*控制倒轉*?呢?如果不是將我們程序的延續交給另一個團體,而是希望它返回給我們一個可以知道它何時完成的能力,然后我們的代碼可以決定下一步做什么呢?
這種規范被稱為?Promise。
Promise正在像風暴一樣席卷JS世界,因為開發者和語言規范作者之流拼命地想要在他們的代碼/設計中結束回調地獄的瘋狂。事實上,大多數新被加入JS/DOM平臺的異步API都是建立在Promise之上的。所以深入學習它們可能是個好主意,你不這么認為嗎?
注意:?“立即”這個詞將在本章頻繁使用,一般來說它指代一些Promise解析行為。然而,本質上在所有情況下,“立即”意味著就工作隊列行為(參見第一章)而言,不是嚴格同步的?*現在*?的感覺。
## 什么是Promise?
當開發者們決定要學習一種新技術或模式的時候,他們的第一步總是“給我看代碼!”。摸著石頭過河對我們來講是十分自然的。
但事實上僅僅考察API丟失了一些抽象過程。Promise是這樣一種工具:它能非常明顯地看出使用者是否理解了它是為什么和關于什么,還是僅僅學習和使用API。
所以在我展示Promise的代碼之前,我想在概念上完整地解釋一下Promise到底是什么。我希望這能更好地指引你探索如何將Promise理論整合到你自己的異步流程中。
帶著這樣的想法,讓我們來看兩種類比,來解釋Promise是什么。
### [](https://github.com/getify/You-Dont-Know-JS/blob/1ed-zh-CN/async%20%26%20performance/ch3.md#%E6%9C%AA%E6%9D%A5%E7%9A%84%E5%80%BC)未來的值
想象這樣的場景:我走到快餐店的柜臺前,點了一個起士漢堡。并交了1.47美元的現金。通過點餐和付款,我為得到一個?*值*(起士漢堡)制造了一個請求。我發起了一個事務。
但是通常來說,起士漢堡不會立即到我手中。收銀員交給一些東西代替我的起士漢堡:一個帶有點餐排隊號的收據。這個點餐號是一個“我欠你”的許諾(Promise),它保證我最終會得到我的起士漢堡。
于是我就拿著我的收據和點餐號。我知道它代表我的?*未來的起士漢堡*,所以我無需再擔心它——除了挨餓!
在我等待的時候,我可以做其他的事情,比如給我的朋友發微信說,“嘿,一塊兒吃午餐嗎?我要吃起士漢堡”。
我已經在用我的?*未來的起士漢堡*?進行推理了,即便它還沒有到我手中。我的大腦可以這么做是因為它將點餐號作為起士漢堡的占位符號。這個占位符號實質上使這個值?*與時間無關*。它是一個?未來的值。
最終,我聽到,“113號!”。于是我愉快地拿著收據走回柜臺前。我把收據遞給收銀員,拿回我的起士漢堡。
換句話說,一旦我的?*未來的值*?準備好,我就用我的許諾值換回值本身。
但還有另外一種可能的輸出。它們叫我的號,但當我去取起士漢堡時,收銀員遺憾地告訴我,“對不起,看起來我們的起士漢堡賣光了。”把這種場景下顧客有多沮喪放在一邊,我們可以看到?*未來的值*?的一個重要性質:它們既可以表示成功也可以表示失敗。
每次我點起士漢堡時,我都知道我要么最終得到一個起士漢堡,要么得到起士漢堡賣光的壞消息,并且不得不考慮中午吃點兒別的東西。
注意:?在代碼中,事情沒有這么簡單,因為還隱含著一種點餐號永遠也不會被叫到的情況,這時我們就被擱置在了一種無限等待的未解析狀態。我們待會兒再回頭處理這種情況。
#### 現在和稍后的值
這一切也許聽起來在思維上太過抽象而不能實施在你的代碼中。那么,讓我們更具體一些。
然而,在我們能介紹Promise是如何以這種方式工作之前,我們先看看我們已經明白的代碼——回調!——是如何處理這些?*未來值*?的。
在你寫代碼來推導一個值時,比如在一個`number`上進行數學操作,不論你是否理解,對于這個值你已經假設了某些非常基礎的事實——這個值已經是一個實在的?*現在*?值:
```source-js
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`相加,但如果它們中任意一個還沒有被設置,就等到它們都被設置。盡快將它們相加。”
你的大腦也許剛剛跳進回調。好吧,那么...
```source-js
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`的例子:
```source-js
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(..)`調用實際上可以接受兩個函數,第一個用作完成(正如剛才所示),而第二個用作拒絕:
```source-js
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(..)`的事件,當我們被通知時,做相應的處理。
首先,考慮一些假想代碼:
```source-js
foo(x) {
// 開始做一些可能會花一段時間的事情
}
foo( 42 )
on (foo "completion") {
// 現在我們可以做下一步了!
}
on (foo "error") {
// 噢,在`foo(..)`中有某些事情搞錯了
}
```
我們調用`foo(..)`然后我們設置兩個事件監聽器,一個給`"completion"`,一個給`"error"`——`foo(..)`調用的兩種可能的最終結果。實質上,`foo(..)`甚至不知道調用它的代碼監聽了這些事件,這構成了一個非常美妙的?*關注分離(separation of concerns)*。
不幸的是,這樣的代碼將需要JS環境不具備的一些“魔法”(而且顯得有些不切實際)。這里是一種用JS表達它的更自然的方式:
```source-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(..)`完成時被獨立地通知,來執行后續的步驟:
```source-js
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(拒絕)"`事件,雖然我們在代碼中不會看到這些名詞被明確地使用。
考慮:
```source-js
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)”](http://domenic.me/2014/02/13/the-revealing-constructor-pattern/)。被傳入的函數被立即執行(不會被異步推遲,像`then(..)`的回調那樣),而且它被提供了兩個參數,我們叫它們`resolve`和`reject`。這些是Promise的解析函數。`resolve(..)`一般表示完成,而`reject(..)`表示拒絕。
你可能猜到了`bar(..)`和`baz(..)`的內部看起來是什么樣子:
```source-js
function bar(fooPromise) {
// 監聽`foo(..)`的完成
fooPromise.then(
function(){
// `foo(..)`現在完成了,那么做`bar(..)`的任務
},
function(){
// 噢,在`foo(..)`中有某些事情搞錯了
}
);
}
// `baz(..)`同上
```
Promise解析沒有必要一定發送消息,就像我們將Promise作為?*未來值*?考察時那樣。它可以僅僅作為一種流程控制信號,就像前面的代碼中那樣使用。
另一種表達方式是:
```source-js
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(..)`返回的promise`p`都被用于控制下一步發生什么。
另外,兩個代碼段都以對同一個promise`p`調用兩次`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的鴨子類型檢查可能大致是這樣:
```source-js
if (
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
) {
// 認為它是一個thenable!
}
else {
// 不是一個thenable
}
```
暈!先把將這種邏輯在各種地方實現有點丑陋的事實放在一邊不談,這里還有更多更深層的麻煩。
如果你試著用一個偶然擁有`then(..)`函數的任意對象/函數來完成一個Promise,但你又沒想把它當做一個Promise/thenable來對待,你的運氣就用光了,因為它會被自動地識別為一個thenable并以特殊的規則來對待(見本章后面的部分)。
如果你不知道一個值上面擁有`then(..)`就更是這樣。比如:
```source-js
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或thenable。它只是一個擁有一些屬性的直白的對象。你可能只是想要把這個值像其他對象那樣傳遞而已。
但你不知道的是,`v`還`[[Prototype]]`連接著(見本叢書的?*this與對象原型*)另一個對象`o`,在它上面偶然擁有一個`then(..)`。所以thenable鴨子類型檢查將會認為并假定`v`是一個thenable。噢。
它甚至不需要直接故意那么做:
```source-js
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"),而且沒有任何在這些回調中發生的事情可以影響/推遲其他回調的調用。
舉例來說:
```source-js
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上的回調之間的相對順序,是不能可靠預測的。
如果兩個promise`p1`和`p2`都準備好被解析了,那么`p1.then(..); p2.then(..)`應當歸結為首先調用`p1`的回調,然后調用`p2`的。但有一些微妙的情形可能會使這不成立,比如下面這樣:
```source-js
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`不是被一個立即值所解析的,而是由另一個promise`p3`所解析,而`p3`本身被一個值`"B"`所解析。這種指定的行為將`p3`*展開*?到`p1`,但是是異步地,所以在異步工作隊列中`p1`的回調位于`p2`的回調之后(參照第一章的"Jobs")。
為了回避這樣的微妙的噩夢,你絕不應該依靠任何跨Promise的回調順序/排程。事實上,一個好的實踐方式是在代碼中根本不要讓多個回調的順序成為問題。盡可能回避它。
### 根本不調回調
這是一個很常見的顧慮。Promise用幾種方式解決它。
首先,沒有任何東西(JS錯誤都不能)可以阻止一個Promise通知你它的解析(如果它被解析了的話)。如果你在一個Promise上同時注冊了完成和拒絕回調,而且這個Promise被解析了,兩個回調中的一個總會被調用。
當然,如果你的回調本身有JS錯誤,你可能不會看到你期望的結果,但是回調事實上已經被調用了。我們待會兒就會講到如何在你的回調中收到關于一個錯誤的通知,因為就算是它們也不會被吞掉。
那如果Promise本身不管怎樣永遠沒有被解析呢?即便是這種狀態Promise也給出了答案,使用一個稱為“競賽(race)”的高級抽象。
```source-js
// 一個使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變為拒絕。
舉例來說:
```source-js
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異常錯誤會怎樣呢?即便是那些也不會丟失,但你可能會發現處理它們的方式有些令人詫異,除非你深挖一些:
```source-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`的解析時發生了錯誤,而在稍后變成一個拒絕。
除了違反原則,這樣的行為還可能造成破壞,假如說有多個在promise`p`上注冊的`then(..)`回調,因為有些會被調用而有些不會,而且至于為什么是很明顯的。
### 可信的Promise?
為了基于Promise模式建立信任,還有最后一個細節需要考察。
無疑你已經注意到了,Promise根本沒有擺脫回調。它們只是改變了回調傳遞的位置。與將一個回調傳入`foo(..)`相反,我們從`foo(..)`那里拿回?*某些東西*?(表面上是一個純粹的Promise),然后我們將回調傳入這個?*東西*。
但為什么這要比僅使用回調的方式更可靠呢?我們如何確信我們拿回來的?*某些東西*?事實上是一個可信的Promise?這難道不是說我們相信它僅僅因為我們已經相信它了嗎?
一個Promise經常被忽視,但是最重要的細節之一,就是它也為這個問題給出了解決方案。包含在原生的ES6`Promise`實現中,它就是`Promise.resolve(..)`。
如果你傳遞一個立即的,非Promise的,非thenable的值給`Promise.resolve(..)`,你會得到一個用這個值完成的promise。換句話說,下面兩個promise`p1`和`p2`的行為基本上完全相同:
```source-js
var p1 = new Promise( function(resolve,reject){
resolve( 42 );
} );
var p2 = Promise.resolve( 42 );
```
但如果你傳遞一個純粹的Promise給`Promise.resolve(..)`,你會得到這個完全相同的promise:
```source-js
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true
```
更重要的是,如果你傳遞一個非Promise的thenable值給`Promise.resolve(..)`,它會試著將這個值展開,而且直到抽出一個最終具體的非Promise值之前,展開操作將會一直繼續下去。
還記得我們先前討論的thenable嗎?
考慮這段代碼:
```source-js
var p = {
then: function(cb) {
cb( 42 );
}
};
// 這工作起來沒問題,但要靠運氣
p
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// 永遠不會跑到這里
}
);
```
這個`p`是一個thenable,但它不是一個純粹的Promise。很走運,它是合理的,正如大多數情況那樣。但是如果你得到的是看起來像這樣的東西:
```source-js
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(..)`,而且我們將會得到一個我們期望的泛化,安全的結果:
```source-js
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包裝器來進行鏈式調用:
```source-js
// 不要只是這么做:
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的完成。
我們首先來說明一下這是什么意思,然后我們將會延伸出它是如何幫助我們創建異步順序的控制流程的。考慮下面的代碼:
```source-js
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`等)有點兒惱人。幸運的是,我們可以簡單地將這些鏈接在一起:
```source-js
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,同樣的展開操作也會發生。考慮這段代碼:
```source-js
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中引入異步,一切還是會同樣正常的工作:
```source-js
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的創建(沒有解析信息)泛化為一個我們可以在多個步驟中復用的工具:
```source-js
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請求:
```source-js
// 假定一個`ajax( {url}, {callback} )`工具
// 帶有Promise的ajax
function request(url) {
return new Promise( function(resolve,reject){
// `ajax(..)`的回調應當是我們的promise的`resolve(..)`函數
ajax( url, resolve );
} );
}
```
我們首先定義一個`request(..)`工具,它構建一個promise表示`ajax(..)`調用的完成:
```source-js
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的,意味著在鏈條的任意一點捕獲這些錯誤是可能的,而且這些捕獲操作在那一點上將鏈條“重置”,使它回到正常的操作上來:
```source-js
// 步驟 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(..)`,而且你只向它傳遞了一個完成處理器,一個假定的拒絕處理器會取而代之:
```source-js
var p = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = p.then(
function fulfilled(){
// 永遠不會跑到這里
}
// 如果忽略或者傳入任何非函數的值,
// 會有假定有一個這樣的拒絕處理器
// function(err) {
// throw err;
// }
);
```
如你所見,這個假定的拒絕處理器僅僅簡單地重新拋出錯誤,它最終強制`p2`(鏈接著的promise)用同樣的錯誤進行拒絕。實質上,它允許錯誤持續地在Promise鏈上傳播,直到遇到一個明確定義的拒絕處理器。
注意:?稍后我們會講到更多關于使用Promise進行錯誤處理的細節,因為會有更多微妙的細節需要關心。
如果沒有一個恰當的合法的函數作為`then(..)`的完成處理器參數,也會有一個默認的處理器取而代之:
```source-js
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(..)`構造器:
```source-js
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方法:
```source-js
var fulfilledPr = Promise.resolve( 42 );
var rejectedPr = Promise.reject( "Oops" );
```
`Promise.resolve(..)`創建了一個Promise,它被解析為它被給予的值。在這個例子中,`42`是一個一般的,非Promise,非thenable的值,所以完成的promise`fulfilledPr`是為值`42`創建的。`Promise.reject("Oops")`為了原因`"Oops"`創建的拒絕的promise`rejectedPr`。
現在讓我們來解釋為什么如果“resolve”這個詞(正如`Promise.resolve(..)`里的)被明確用于一個既可能完成也可能拒絕的環境時,它沒有歧義,反而更加準確:
```source-js
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:
```source-js
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(..)`:
```source-js
function fulfilled(msg) {
console.log( msg );
}
function rejected(err) {
console.error( err );
}
p.then(
fulfilled,
rejected
);
```
對于`then(..)`的第一個參數的情況,它沒有歧義地總是完成狀態,所以沒有必要使用帶有雙重意義的“resolve”術語。另一方面,ES6語言規范中使用`onFulfilled(..)`和`onRejected(..)`?來標識這兩個回調,所以它們是準確的術語。
## 錯誤處理
我們已經看過幾個例子,Promise拒絕——既可以通過有意調用`reject(..)`,也可以通過意外的JS異常——是如何在異步編程中允許清晰的錯誤處理的。讓我們兜個圈子回去,將我們一帶而過的一些細節弄清楚。
對大多數開發者來說,最自然的錯誤處理形式是同步的`try..catch`結構。不幸的是,它僅能用于同步狀態,所以在異步代碼模式中它幫不上什么忙:
```source-js
function foo() {
setTimeout( function(){
baz.bar();
}, 100 );
}
try {
foo();
// 稍后會從`baz.bar()`拋出全局錯誤
}
catch (err) {
// 永遠不會到這里
}
```
能有`try..catch`當然很好,但除非有某些附加的環境支持,它無法與異步操作一起工作。我們將會在第四章中討論generator時回到這個話題。
在回調中,對于錯誤處理的模式已經有了一些新興的模式,最有名的就是“錯誤優先回調”風格:
```source-js
function foo(cb) {
setTimeout( function(){
try {
var x = baz.bar();
cb( null, x ); // 成功!
}
catch (err) {
cb( err );
}
}, 100 );
}
foo( function(err,val){
if (err) {
console.error( err ); // 倒霉 :(
}
else {
console.log( val );
}
} );
```
注意:?這里的`try..catch`僅在`baz.bar()`調用立即地,同步地成功或失敗時才能工作。如果`baz.bar()`本身是一個異步完成的函數,它內部的任何異步錯誤都不能被捕獲。
我們傳遞給`foo(..)`的回調期望通過預留的`err`參數收到一個表示錯誤的信號。如果存在,就假定出錯。如果不存在,就假定成功。
這類錯誤處理在技術上是?*異步兼容的*,但它根本組織的不好。用無處不在的`if`語句檢查將多層錯誤優先回調編織在一起,將不可避免地將你置于回調地獄的危險之中(見第二章)。
那么我們回到Promise的錯誤處理,使用傳遞給`then(..)`的拒絕處理器。Promise不使用流行的“錯誤優先回調”設計風格,反而使用“分割回調”的風格;一個回調給完成,一個回調給拒絕:
```source-js
var p = Promise.reject( "Oops" );
p.then(
function fulfilled(){
// 永遠不會到這里
},
function rejected(err){
console.log( err ); // "Oops"
}
);
```
雖然這種模式表面上看起來十分有道理,但是Promise錯誤處理的微妙之處經常使它有點兒相當難以全面把握。
考慮下面的代碼:
```source-js
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 數字沒有字符串方法,
// 所以這里拋出一個錯誤
console.log( msg.toLowerCase() );
},
function rejected(err){
// 永遠不會到這里
}
);
```
如果`msg.toLowerCase()`合法地拋出一個錯誤(它會的!),為什么我們的錯誤處理器沒有得到通知?正如我們早先解釋的,這是因為?*這個*?錯誤處理器是為`p`promise準備的,也就是已經被值`42`完成的那個promise。`p`promise是不可變的,所以唯一可以得到錯誤通知的promise是由`p.then(..)`返回的那個,而在這里我們沒有捕獲它。
這應當解釋了:為什么Promise的錯誤處理是易錯的。錯誤太容易被吞掉了,而這很少是你有意這么做的。
警告:?如果你以一種不合法的方式使用Promise API,而且有錯誤阻止正常的Promise構建,其結果將是一個立即被拋出的異常,而不是一個拒絕Promise。這是一些導致Promise構建失敗的錯誤用法:`new Promise(null)`,`Promise.all()`,`Promise.race(42)`等等。如果你沒有足夠合法地使用Promise API來首先實際構建一個Promise,你就不能得到一個拒絕Promise!
### 絕望的深淵
幾年前Jeff Atwood曾經寫到:編程語言總是默認地以這樣的方式建立,開發者們會掉入“絕望的深淵”([http://blog.codinghorror.com/falling-into-the-pit-of-success/](http://blog.codinghorror.com/falling-into-the-pit-of-success/)?)——在這里意外會被懲罰——而你不得不更努力地使它正確。他懇求我們相反地創建“成功的深淵”,就是你會默認地掉入期望的(成功的)行為,而如此你不得不更努力地去失敗。
毫無疑問,Promise的錯誤處理是一種“絕望的深淵”的設計。默認情況下,它假定你想讓所有的錯誤都被Promise的狀態吞掉,而且如果你忘記監聽這個狀態,錯誤就會默默地凋零/死去——通常是絕望的。
為了回避把一個被遺忘/拋棄的Promise的錯誤無聲地丟失,一些開發者宣稱Promise鏈的“最佳實踐”是,總是將你的鏈條以`catch(..)`終結,就像這樣:
```source-js
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 數字沒有字符串方法,
// 所以這里拋出一個錯誤
console.log( msg.toLowerCase() );
}
)
.catch( handleErrors );
```
因為我們沒有給`then(..)`傳遞拒絕處理器,默認的處理器會頂替上來,它僅僅簡單地將錯誤傳播到鏈條的下一個promise中。如此,在`p`中發生的錯誤,與在`p`之后的解析中(比如`msg.toLowerCase()`)發生的錯誤都將會過濾到最后的`handleErrors(..)`中。
問題解決了,對吧?沒那么容易!
要是`handleErrors(..)`本身也有錯誤呢?誰來捕獲它?這里還有一個沒人注意的promise:`catch(..)`返回的promise,我們沒有對它進行捕獲,也沒注冊拒絕處理器。
你不能僅僅將另一個`catch(..)`貼在鏈條末尾,因為它也可能失敗。Promise鏈的最后一步,無論它是什么,總有可能,即便這種可能性逐漸減少,懸掛著一個困在未被監聽的Promise中的,未被捕獲的錯誤。
聽起來像一個不可解的迷吧?
### 處理未被捕獲的錯誤
這不是一個很容易就能完全解決的問題。但是有些接近于解決的方法,或者說?*更好的方法*。
一些Promise庫有一些附加的方法,可以注冊某些類似于“全局的未處理拒絕”的處理器,全局上不會拋出錯誤,而是調用它。但是他們識別一個錯誤是“未被捕獲的錯誤”的方案是,使用一個任意長的計時器,比如說3秒,從拒絕的那一刻開始計時。如果一個Promise被拒絕但沒有錯誤處理在計時器被觸發前注冊,那么它就假定你不會注冊監聽器了,所以它是“未被捕獲的”。
實踐中,這個方法在許多庫中工作的很好,因為大多數用法不會在Promise拒絕和監聽這個拒絕之間有很明顯的延遲。但是這個模式有點兒麻煩,因為3秒實在太隨意了(即便它是實證過的),還因為確實有些情況你想讓一個Promise在一段不確定的時間內持有它的拒絕狀態,而且你不希望你的“未捕獲錯誤”處理器因為這些誤報(還沒處理的“未捕獲錯誤”)而被調用。
另一種常見的建議是,Promise應當增加一個`done(..)`方法,它實質上標志著Promise鏈的“終結”。`done(..)`不會創建并返回一個Promise,所以傳遞給`done(..)`的回調很明顯地不會鏈接上一個不存在的Promise鏈,并向它報告問題。
那么接下來會發什么?正如你通常在未處理錯誤狀態下希望的那樣,在`done(..)`的拒絕處理器內部的任何異常都作為全局的未捕獲錯誤拋出(基本上扔到開發者控制臺):
```source-js
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 數字沒有字符串方法,
// 所以這里拋出一個錯誤
console.log( msg.toLowerCase() );
}
)
.done( null, handleErrors );
// 如果`handleErrors(..)`自身發生異常,它會在這里被拋出到全局
```
這聽起來要比永不終結的鏈條或隨意的超時要吸引人。但最大的問題是,它不是ES6標準,所以不管聽起來多么好,它成為一個可靠而普遍的解決方案還有很長的距離。
那我們就卡在這里了?不完全是。
瀏覽器有一個我們的代碼沒有的能力:它們可以追蹤并確定一個對象什么時候被廢棄并可以作為垃圾回收。所以,瀏覽器可以追蹤Promise對象,當它們被當做垃圾回收時,如果在它們內部存在一個拒絕狀態,瀏覽器就可以確信這是一個合法的“未捕獲錯誤”,它可以信心十足地知道應當在開發者控制臺上報告這一情況。
注意:?在寫作本書的時候,Chrome和Firefox都早已試圖實現這種“未捕獲拒絕”的能力,雖然至多也就是支持的不完整。
然而,如果一個Promise不被垃圾回收——通過許多不同的代碼模式,這極其容易不經意地發生——瀏覽器的垃圾回收檢測不會幫你知道或診斷你有一個拒絕的Promise靜靜地躺在附近。
還有其他選項嗎?有。
### 成功的深淵
以下講的僅僅是理論上,Promise?*可能*?在某一天變成什么樣的行為。我相信那會比我們現在擁有的優越許多。而且我想這種改變可能會發生在后ES6時代,因為我不認為它會破壞Web的兼容性。另外,如果你小心行事,它是可以被填補(polyfilled)/預填補(prollyfilled)的。讓我們來看一下:
* Promise可以默認為是報告(向開發者控制臺)一切拒絕的,就在下一個Job或事件輪詢tick,如果就在這時Promise上沒有注冊任何錯誤處理器。
* 如果你希望拒絕的Promise在被監聽前,將其拒絕狀態保持一段不確定的時間。你可以調用`defer()`,它會壓制這個Promise自動報告錯誤。
如果一個Promise被拒絕,默認地它會吵吵鬧鬧地向開發者控制臺報告這個情況(而不是默認不出聲)。你既可以選擇隱式地處理這個報告(通過在拒絕之前注冊錯誤處理器),也可以選擇明確地處理這個報告(使用`defer()`)。無論哪種情況,*你*?都控制著這種誤報。
考慮下面的代碼:
```source-js
var p = Promise.reject( "Oops" ).defer();
// `foo(..)`返回Promise
foo( 42 )
.then(
function fulfilled(){
return p;
},
function rejected(err){
// 處理`foo(..)`的錯誤
}
);
...
```
我們創建了`p`,我們知道我們會為了使用/監聽它的拒絕而等待一會兒,所以我們調用`defer()`——如此就不會有全局的報告。`defer()`單純地返回同一個promise,為了鏈接的目的。
從`foo(..)`返回的promise?*當即*?就添附了一個錯誤處理器,所以這隱含地跳出了默認行為,而且不會有全局的關于錯誤的報告。
但是從`then(..)`調用返回的promise沒有`defer()`或添附錯誤處理器,所以如果它被拒絕(從它內部的任意一個解析處理器中),那么它就會向開發者控制臺報告一個未捕獲錯誤。
這種設計稱為成功的深淵。默認情況下,所有的錯誤不是被處理就是被報告——這幾乎是所有開發者在幾乎所有情況下所期望的。你要么不得不注冊一個監聽器,要么不得不有意什么都不做,并指示你要將錯誤處理推遲到?*稍后*;你僅為這種特定情況選擇承擔額外的責任。
這種方式唯一真正的危險是,你`defer()`了一個Promise但是實際上沒有監聽/處理它的拒絕。
但你不得不有意地調用`defer()`來選擇進入絕望深淵——默認是成功深淵——所以對于從你自己的錯誤中拯救你這件事來說,我們能做的不多。
我覺得對于Promise的錯誤處理還有希望(在后ES6時代)。我希望上層人物將會重新思考這種情況并考慮選用這種方式。同時,你可以自己實現這種方式(給讀者們的挑戰練習!),或使用一個?*聰明*?的Promise庫來為你這么做。
注意:?這種錯誤處理/報告的確切的模型已經在我的?*asynquence*?Promise抽象庫中實現,我們會在本書的附錄A中討論它。
## Promise模式
我們已經隱含地看到了使用Promise鏈的順序模式(這個-然后-這個-然后-那個的流程控制),但是我們還可以在Promise的基礎上抽象出許多其他種類的異步模式。這些模式用于簡化異步流程控制的的表達——它可以使我們的代碼更易于推理并且更易于維護——即便是我們程序中最復雜的部分。
有兩個這樣的模式被直接編碼在ES6原生的`Promise`實現中,所以我們免費的得到了它們,來作為我們其他模式的構建塊兒。
### Promise.all([ .. ])
在一個異步序列(Promise鏈)中,在任何給定的時刻都只有一個異步任務在被協調——第2步嚴格地接著第1步,而第3步嚴格地接著第2步。但要是并發(也叫“并行地”)地去做兩個或以上的步驟呢?
用經典的編程術語,一個“門(gate)”是一種等待兩個或更多并行/并發任務都執行完再繼續的機制。它們完成的順序無關緊要,只是它們不得不都完成才能讓門打開,繼而讓流程控制通過。
在Promise API中,我們稱這種模式為`all([ .. ])`。
比方說你想同時發起兩個Ajax請求,在發起第三個Ajax請求發起之前,等待它們都完成,而不管它們的順序。考慮這段代碼:
```source-js
// `request(..)`是一個兼容Promise的Ajax工具
// 就像我們在本章早前定義的
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] )
.then( function(msgs){
// `p1`和`p2`都已完成,這里將它們的消息傳入
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} )
.then( function(msg){
console.log( msg );
} );
```
`Promise.all([ .. ])`期待一個單獨的參數,一個`array`,一般由Promise的實例組成。從`Promise.all([ .. ])`返回的promise將會收到完成的消息(在這段代碼中是`msgs`),它是一個由所有被傳入的promise的完成消息按照被傳入的順序構成的`array`(與完成的順序無關)。
注意:?技術上講,被傳入`Promise.all([ .. ])`的`array`的值可以包括Promise,thenable,甚至是立即值。這個列表中的每一個值都實質上通過`Promise.resolve(..)`來確保它是一個可以被等待的純粹的Promise,所以一個立即值將被范化為這個值的一個Promise。如果這個`array`是空的,主Promise將會立即完成。
從`Promise.resolve(..)`返回的主Promise將會在所有組成它的promise完成之后才會被完成。如果其中任意一個promise被拒絕,`Promise.all([ .. ])`的主Promise將立即被拒絕,并放棄所有其他promise的結果。
要記得總是給每個promise添加拒絕/錯誤處理器,即使和特別是那個從`Promise.all([ .. ])`返回的promise。
### Promise.race([ .. ])
雖然`Promise.all([ .. ])`并發地協調多個Promise并假定它們都需要被完成,但是有時候你只想應答“沖過終點的第一個Promise”,而讓其他的Promise被丟棄。
這種模式經典地被稱為“閂”,但在Promise中它被稱為一個“競合(race)”。
警告:?雖然“只有第一個沖過終點的算贏”是一個非常合適被比喻,但不幸的是“競合(race)”是一個被占用的詞,因為“競合狀態(race conditions)”通常被認為是程序中的Bug(見第一章)。不要把`Promise.race([ .. ])`與“競合狀態(race conditions)”搞混了。
“競合狀態(race conditions)”也期待一個單獨的`array`參數,含有一個或多個Promise,thenable,或立即值。與立即值進行競合并沒有多大實際意義,因為很明顯列表中的第一個會勝出——就像賽跑時有一個選手在終點線上起跑!
和`Promise.all([ .. ])`相似,`Promise.race([ .. ])`將會在任意一個Promise解析為完成時完成,而且它會在任意一個Promise解析為拒絕時拒絕。
注意:?一個“競合(race)”需要至少一個“選手”,所以如果你傳入一個空的`array`,`race([..])`的主Promise將不會立即解析,反而是永遠不會被解析。這是砸自己的腳!ES6應當將它規范為要么完成,要么拒絕,或者要么拋出某種同步錯誤。不幸的是,因為在ES6的`Promise`之前的Promise庫的優先權高,他們不得不把這個坑留在這兒,所以要小心絕不要傳入一個空`array`。
讓我們重溫剛才的并發Ajax的例子,但是在`p1`和`p2`競合的環境下:
```source-js
// `request(..)`是一個兼容Promise的Ajax工具
// 就像我們在本章早前定義的
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
.then( function(msg){
// `p1`或`p2`會贏得競合
return request(
"http://some.url.3/?v=" + msg
);
} )
.then( function(msg){
console.log( msg );
} );
```
因為只有一個Promise會勝出,所以完成的值是一個單獨的消息,而不是一個像`Promise.all([ .. ])`中那樣的`array`。
#### 超時競合
我們早先看過這個例子,描述`Promise.race([ .. ])`如何能夠用于表達“promise超時”模式:
```source-js
// `foo()`是一個兼容Promise
// `timeoutPromise(..)`在早前定義過,
// 返回一個在指定延遲之后會被拒絕的Promise
// 為`foo()`設置一個超時
Promise.race( [
foo(), // 嘗試`foo()`
timeoutPromise( 3000 ) // 給它3秒鐘
] )
.then(
function(){
// `foo(..)`及時地完成了!
},
function(err){
// `foo()`要么是被拒絕了,要么就是沒有及時完成
// 可以考察`err`來知道是哪一個原因
}
);
```
這種超時模式在絕大多數情況下工作的很好。但這里有一些微妙的細節要考慮,而且坦率的說它們對于`Promise.race([ .. ])`和`Promise.all([ .. ])`都同樣需要考慮。
#### "Finally"
要問的關鍵問題是,“那些被丟棄/忽略的promise發生了什么?”我們不是從性能的角度在問這個問題——它們通常最終會變成垃圾回收的合法對象——而是從行為的角度(副作用等等)。Promise不能被取消——而且不應當被取消,因為那會摧毀本章稍后的“Promise不可取消”一節中要討論的外部不可變性——所以它們只能被無聲地忽略。
但如果前面例子中的`foo()`占用了某些資源,但超時首先觸發而且導致這個promise被忽略了呢?這種模式中存在某種東西可以在超時后主動釋放被占用的資源,或者取消任何它可能帶來的副作用嗎?要是你想做的全部只是記錄下`foo()`超時的事實呢?
一些開發者提議,Promise需要一個`finally(..)`回調注冊機制,它總是在Promise解析時被調用,而且允許你制定任何可能的清理操作。在當前的語言規范中它還不存在,但它可能會在ES7+中加入。我們不得不邊走邊看了。
它看起來可能是這樣:
```source-js
var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );
```
注意:?在各種Promise庫中,`finally(..)`依然會創建并返回一個新的Promise(為了使鏈條延續下去)。如果`cleanup(..)`函數返回一個Promise,它將會鏈入鏈條,這意味著你可能還有我們剛才討論的未處理拒絕的問題。
同時,我們可以制造一個靜態的幫助工具來讓我們觀察(但不干涉)Promise的解析:
```source-js
// 填補的安全檢查
if (!Promise.observe) {
Promise.observe = function(pr,cb) {
// 從側面觀察`pr`的解析
pr.then(
function fulfilled(msg){
// 異步安排回調(作為Job)
Promise.resolve( msg ).then( cb );
},
function rejected(err){
// 異步安排回調(作為Job)
Promise.resolve( err ).then( cb );
}
);
// 返回原本的promise
return pr;
};
}
```
這是我們在前面的超時例子中如何使用它:
```source-js
Promise.race( [
Promise.observe(
foo(), // 嘗試`foo()`
function cleanup(msg){
// 在`foo()`之后進行清理,即便它沒有及時完成
}
),
timeoutPromise( 3000 ) // 給它3秒鐘
] )
```
這個`Promise.observe(..)`幫助工具只是描述你如何在不干擾Promise的情況下觀測它的完成。其他的Promise庫有他們自己的解決方案。不論你怎么做,你都將很可能有個地方想用來確認你的Promise沒有意外地被無聲地忽略掉。
### all([ .. ]) 與 race([ .. ]) 的變種
原生的ES6Promise帶有內建的`Promise.all([ .. ])`和`Promise.race([ .. ])`,這里還有幾個關于這些語義的其他常用的變種模式:
* `none([ .. ])`很像`all([ .. ])`,但是完成和拒絕被轉置了。所有的Promise都需要被拒絕——拒絕變成了完成值,反之亦然。
* `any([ .. ])`很像`all([ .. ])`,但它忽略任何拒絕,所以只有一個需要完成即可,而不是它們所有的。
* `first([ .. ])`像是一個帶有`any([ .. ])`的競合,它忽略任何拒絕,而且一旦有一個Promise完成時,它就立即完成。
* `last([ .. ])`很像`first([ .. ])`,但是只有最后一個完成勝出。
某些Promise抽象工具庫提供這些方法,但你也可以用Promise機制的`race([ .. ])`和`all([ .. ])`,自己定義他們。
比如,這是我們如何定義`first([..])`:
```source-js
// 填補的安全檢查
if (!Promise.first) {
Promise.first = function(prs) {
return new Promise( function(resolve,reject){
// 迭代所有的promise
prs.forEach( function(pr){
// 泛化它的值
Promise.resolve( pr )
// 無論哪一個首先成功完成,都由它來解析主promise
.then( resolve );
} );
} );
};
}
```
注意:?這個`first(..)`的實現不會在它所有的promise都被拒絕時拒絕;它會簡單地掛起,很像`Promise.race([])`。如果需要,你可以添加一些附加邏輯來追蹤每個promise的拒絕,而且如果所有的都被拒絕,就在主promise上調用`reject()`。我們將此作為練習留給讀者。
### 并發迭代
有時候你想迭代一個Promise的列表,并對它們所有都實施一些任務,就像你可以對同步的`array`做的那樣(比如,`forEach(..)`,`map(..)`,`some(..)`,和`every(..)`)。如果對每個Promise實施的操作根本上是同步的,它們工作的很好,正如我們在前面的代碼段中用過的`forEach(..)`。
但如果任務在根本上是異步的,或者可以/應當并發地實施,你可以使用許多庫提供的異步版本的這些工具方法。
比如,讓我們考慮一個異步的`map(..)`工具,它接收一個`array`值(可以是Promise或任何東西),外加一個對數組中每一個值實施的函數(任務)。`map(..)`本身返回一個promise,它的完成值是一個持有每個任務的異步完成值的`array`(以與映射(mapping)相同的順序):
```source-js
if (!Promise.map) {
Promise.map = function(vals,cb) {
// 一個等待所有被映射的promise的新promise
return Promise.all(
// 注意:普通的數組`map(..)`,
// 將值的數組變為promise的數組
vals.map( function(val){
// 將`val`替換為一個在`val`
// 異步映射完成后才解析的新promise
return new Promise( function(resolve){
cb( val, resolve );
} );
} )
);
};
}
```
注意:?在這種`map(..)`的實現中,你無法表示異步拒絕,但如果一個在映射的回調內部發生一個同步的異常/錯誤,那么`Promise.map(..)`返回的主Promise就會拒絕。
讓我們描繪一下對一組Promise(不是簡單的值)使用`map(..)`:
```source-js
var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Oops" );
// 將列表中的值翻倍,即便它們在Promise中
Promise.map( [p1,p2,p3], function(pr,done){
// 確保列表中每一個值都是Promise
Promise.resolve( pr )
.then(
// 將值作為`v`抽取出來
function(v){
// 將完成的`v`映射到新的值
done( v * 2 );
},
// 或者,映射到promise的拒絕消息上
done
);
} )
.then( function(vals){
console.log( vals ); // [42,84,"Oops"]
} );
```
## Promise API概覽
讓我們復習一下我們已經在本章中零散地展開的ES6`Promise`API。
注意:?下面的API盡管在ES6中是原生的,但也存在一些語言規范兼容的填補(不光是擴展Promise庫),它們定義了`Promise`和與之相關的所有行為,所以即使是在前ES6時代的瀏覽器中你也以使用原生的Promise。這類填補的其中之一是“Native Promise Only”([http://github.com/getify/native-promise-only),我寫的!](http://github.com/getify/native-promise-only)
### new Promise(..)構造器
*揭示構造器(revealing constructor)*?`Promise(..)`必須與`new`一起使用,而且必須提供一個被同步/立即調用的回調函數。這個函數被傳入兩個回調函數,它們作為promise的解析能力。我們通常將它們標識為`resolve(..)`和`reject(..)`:
```source-js
var p = new Promise( function(resolve,reject){
// `resolve(..)`給解析/完成的promise
// `reject(..)`給拒絕的promise
} );
```
`reject(..)`簡單地拒絕promise,但是`resolve(..)`既可以完成promise,也可以拒絕promise,這要看它被傳入什么值。如果`resolve(..)`被傳入一個立即的,非Promise,非thenable的值,那么這個promise將用這個值完成。
但如果`resolve(..)`被傳入一個Promise或者thenable的值,那么這個值將被遞歸地展開,而且無論它最終解析結果/狀態是什么,都將被promise采用。
### Promise.resolve(..) 和 Promise.reject(..)
一個用于創建已被拒絕的Promise的簡便方法是`Promise.reject(..)`,所以這兩個promise是等價的:
```source-js
var p1 = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = Promise.reject( "Oops" );
```
與`Promise.reject(..)`相似,`Promise.resolve(..)`通常用來創建一個已完成的Promise。然而,`Promise.resolve(..)`還會展開thenale值(就像我們已經幾次討論過的)。在這種情況下,返回的Promise將會采用你傳入的thenable的解析,它既可能是完成,也可能是拒絕:
```source-js
var fulfilledTh = {
then: function(cb) { cb( 42 ); }
};
var rejectedTh = {
then: function(cb,errCb) {
errCb( "Oops" );
}
};
var p1 = Promise.resolve( fulfilledTh );
var p2 = Promise.resolve( rejectedTh );
// `p1`將是一個完成的promise
// `p2`將是一個拒絕的promise
```
而且要記住,如果你傳入一個純粹的Promise,`Promise.resolve(..)`不會做任何事情;它僅僅會直接返回這個值。所以在你不知道其本性的值上調用`Promise.resolve(..)`不會有額外的開銷,如果它偶然已經是一個純粹的Promise。
### then(..) 和 catch(..)
每個Promise實例(不是?`Promise`?API 名稱空間)都有`then(..)`和`catch(..)`方法,它們允許你為Promise注冊成功或拒絕處理器。一旦Promise被解析,它們中的一個就會被調用,但不是都會被調用,而且它們總是會被異步地調用(參見第一章的“Jobs”)。
`then(..)`接收兩個參數,第一個用于完成回調,第二個用戶拒絕回調。如果它們其中之一被省略,或者被傳入一個非函數的值,那么一個默認的回調就會分別頂替上來。默認的完成回調簡單地將值向下傳遞,而默認的拒絕回調簡單地重新拋出(傳播)收到的拒絕理由。
`catch(..)`僅僅接收一個拒絕回調作為參數,而且會自動的頂替一個默認的成功回調,就像我們討論過的。換句話說,它等價于`then(null,..)`:
```source-js
p.then( fulfilled );
p.then( fulfilled, rejected );
p.catch( rejected ); // 或者`p.then( null, rejected )`
```
`then(..)`和`catch(..)`也會創建并返回一個新的promise,它可以用來表達Promise鏈式流程控制。如果完成或拒絕回調有異常被拋出,這個返回的promise就會被拒絕。如果這兩個回調之一返回一個立即,非Promise,非thenable值,那么這個值就會作為被返回的promise的完成。如果完成處理器指定地返回一個promise或thenable值這個值就會被展開而且變成被返回的promise的解析。
### Promise.all([ .. ]) 和 Promise.race([ .. ])
在ES6的`Promise`API的靜態幫助方法`Promise.all([ .. ])`和`Promise.race([ .. ])`都創建一個Promise作為它們的返回值。這個promise的解析完全由你傳入的promise數組控制。
對于`Promise.all([ .. ])`,為了被返回的promise完成,所有你傳入的promise都必須完成。如果其中任意一個被拒絕,返回的主promise也會立即被拒絕(丟棄其他所有promise的結果)。至于完成狀態,你會收到一個含有所有被傳入的promise的完成值的`array`。至于拒絕狀態,你僅會收到第一個promise拒絕的理由值。這種模式通常稱為“門”:在門打開前所有人都必須到達。
對于`Promise.race([ .. ])`,只有第一個解析(成功或拒絕)的promise會“勝出”,而且不論解析的結果是什么,都會成為被返回的promise的解析結果。這種模式通常成為“閂”:第一個打開門閂的人才能進來。考慮這段代碼:
```source-js
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( "Hello World" );
var p3 = Promise.reject( "Oops" );
Promise.race( [p1,p2,p3] )
.then( function(msg){
console.log( msg ); // 42
} );
Promise.all( [p1,p2,p3] )
.catch( function(err){
console.error( err ); // "Oops"
} );
Promise.all( [p1,p2] )
.then( function(msgs){
console.log( msgs ); // [42,"Hello World"]
} );
```
警告:?要小心!如果一個空的`array`被傳入`Promise.all([ .. ])`,它會立即完成,但`Promise.race([ .. ])`卻會永遠掛起,永遠不會解析。
ES6的`Promise`API十分簡單和直接。對服務于大多數基本的異步情況來說它足夠好了,而且當你要把你的代碼從回調地獄變為某些更好的東西時,它是一個開始的好地方。
但是依然還有許多應用程序所要求的精巧的異步處理,由于Promise本身所受的限制而不能解決。在下一節中,為了有效利用Promise庫,我們將深入檢視這些限制。
## Promise的限制
本節中我們將要討論的許多細節已經在這一章中被提及了,但我們將明確地復習這些限制。
### 順序的錯誤處理
我們在本章前面的部分詳細講解了Promise風格的錯誤處理。Promise的設計方式——特別是他們如何鏈接——所產生的限制,創建了一個非常容易掉進去的陷阱,Promise鏈中的錯誤會被意外地無聲地忽略掉。
但關于Promise的錯誤還有一些其他事情要考慮。因為Promise鏈只不過是將組成它的Promise連在一起,沒有一個實體可以用來將整個鏈條表達為一個單獨的?*東西*,這意味著沒有外部的方法能夠監聽可能發生的任何錯誤。
如果你構建一個不包含錯誤處理器的Promise鏈,這個鏈條的任意位置發生的任何錯誤都將沿著鏈條向下無限傳播,直到被監聽為止(通過在某一步上注冊拒絕處理器)。所以,在這種特定情況下,擁有鏈條的最后一個promise的引用就夠了(下面代碼段中的`p`),因為你可以在這里注冊拒絕處理器,而且它會被所有傳播的錯誤通知:
```source-js
// `foo(..)`, `STEP2(..)` 和 `STEP3(..)`
// 都是promise兼容的工具
var p = foo( 42 )
.then( STEP2 )
.then( STEP3 );
```
雖然這看起來有點兒小糊涂,但是這里的`p`沒有指向鏈條中的第一個promise(`foo(42)`調用中來的那一個),而是指向了最后一個promise,來自于`then(STEP3)`調用的那一個。
另外,這個promise鏈條上看不到一個步驟做了自己的錯誤處理。這意味著你可以在`p`上注冊一個拒絕處理器,如果在鏈條的任意位置發生了錯誤,它就會被通知。
```source-js
p.catch( handleErrors );
```
但如果這個鏈條中的某一步事實上做了自己的錯誤處理(也許是隱藏/抽象出去了,所以你看不到),那么你的`handleErrors(..)`就不會被通知。這可能是你想要的——它畢竟是一個“被處理過的拒絕”——但它也可能?*不*?是你想要的。完全缺乏被通知的能力(被“已處理過的”拒絕錯誤通知)是一個在某些用法中約束功能的一種限制。
它基本上和`try..catch`中存在的限制是相同的,它可以捕獲一個異常并簡單地吞掉。所以這不是一個?Promise特有?的問題,但它確實是一個我們希望繞過的限制。
不幸的是,許多時候Promise鏈序列的中間步驟不會被留下引用,所以沒有這些引用,你就不能添加錯誤處理器來可靠地監聽錯誤。
### 單獨的值
根據定義,Promise只能有一個單獨的完成值或一個單獨的拒絕理由。在簡單的例子中,這沒什么大不了的,但在更精巧的場景下,你可能發現這個限制。
通常的建議是構建一個包裝值(比如`object`或`array`)來包含這些多個消息。這個方法好用,但是在你的Promise鏈的每一步上把消息包裝再拆開顯得十分尷尬和煩人。
#### 分割值
有時你可以將這種情況當做一個信號,表示你可以/應當將問題拆分為兩個或更多的Promise。
想象你有一個工具`foo(..)`,它異步地產生兩個值(`x`和`y`):
```source-js
function getY(x) {
return new Promise( function(resolve,reject){
setTimeout( function(){
resolve( (3 * x) - 1 );
}, 100 );
} );
}
function foo(bar,baz) {
var x = bar * baz;
return getY( x )
.then( function(y){
// 將兩個值包裝近一個容器
return [x,y];
} );
}
foo( 10, 20 )
.then( function(msgs){
var x = msgs[0];
var y = msgs[1];
console.log( x, y ); // 200 599
} );
```
首先,讓我們重新安排一下`foo(..)`返回的東西,以便于我們不必再將`x`和`y`包裝進一個單獨的`array`值中來傳送給一個Promise。相反,我們將每一個值包裝進它自己的promise:
```source-js
function foo(bar,baz) {
var x = bar * baz;
// 將兩個promise返回
return [
Promise.resolve( x ),
getY( x )
];
}
Promise.all(
foo( 10, 20 )
)
.then( function(msgs){
var x = msgs[0];
var y = msgs[1];
console.log( x, y );
} );
```
一個promise的`array`真的要比傳遞給一個單獨的Promise的值的`array`要好嗎?語法上,它沒有太多改進。
但是這種方式更加接近于Promise的設計原理。現在它更易于在未來將`x`與`y`的計算分開,重構進兩個分離的函數中。它更清晰,也允許調用端代碼更靈活地安排這兩個promise——這里使用了`Promise.all([ .. ])`,但它當然不是唯一的選擇——而不是將這樣的細節在`foo(..)`內部進行抽象。
#### 展開/散開參數
`var x = ..`和`var y = ..`的賦值依然是一個尷尬的負擔。我們可以在一個幫助工具中利用一些函數式技巧(向Reginald Braithwaite致敬,在推特上 @raganwald ):
```source-js
function spread(fn) {
return Function.apply.bind( fn, null );
}
Promise.all(
foo( 10, 20 )
)
.then(
spread( function(x,y){
console.log( x, y ); // 200 599
} )
)
```
看起來好些了!當然,你可以內聯這個函數式魔法來避免額外的幫助函數:
```source-js
Promise.all(
foo( 10, 20 )
)
.then( Function.apply.bind(
function(x,y){
console.log( x, y ); // 200 599
},
null
) );
```
這個技巧可能很整潔,但是ES6給了我們一個更好的答案:解構(destructuring)。數組的解構賦值形式看起來像這樣:
```source-js
Promise.all(
foo( 10, 20 )
)
.then( function(msgs){
var [x,y] = msgs;
console.log( x, y ); // 200 599
} );
```
最棒的是,ES6提供了數組參數解構形式:
```source-js
Promise.all(
foo( 10, 20 )
)
.then( function([x,y]){
console.log( x, y ); // 200 599
} );
```
我們現在已經接受了“每個Promise一個值”的準則,繼續讓我們把模板代碼最小化!
注意:?更多關于ES6解構形式的信息,參閱本系列的?*ES6與未來*。
### 單次解析
Promise的一個最固有的行為之一就是,一個Promise只能被解析一次(成功或拒絕)。對于多數異步用例來說,你僅僅取用這個值一次,所以這工作的很好。
但也有許多異步情況適用于一個不同的模型——更類似于事件和/或數據流。表面上看不清Promise能對這種用例適應的多好,如果能的話。沒有基于Promise的重大抽象過程,它們完全缺乏對多個值解析的處理。
想象這樣一個場景,你可能想要為響應一個刺激(比如事件)觸發一系列異步處理步驟,而這實際上將會發生多次,比如按鈕點擊。
這可能不會像你想的那樣工作:
```source-js
// `click(..)` 綁定了一個DOM元素的 `"click"` 事件
// `request(..)` 是先前定義的支持Promise的Ajax
var p = new Promise( function(resolve,reject){
click( "#mybtn", resolve );
} );
p.then( function(evt){
var btnID = evt.currentTarget.id;
return request( "http://some.url.1/?id=" + btnID );
} )
.then( function(text){
console.log( text );
} );
```
這里的行為僅能在你的應用程序只讓按鈕被點擊一次的情況下工作。如果按鈕被點擊第二次,promise`p`已經被解析了,所以第二個`resolve(..)`將被忽略。
相反的,你可能需要將模式反過來,在每次事件觸發時創建一個全新的Promise鏈:
```source-js
click( "#mybtn", function(evt){
var btnID = evt.currentTarget.id;
request( "http://some.url.1/?id=" + btnID )
.then( function(text){
console.log( text );
} );
} );
```
這種方式會?*好用*,為每個按鈕上的`"click"`事件發起一個全新的Promise序列。
但是除了在事件處理器內部定義一整套Promise鏈看起來很丑以外,這樣的設計在某種意義上違背了關注/能力分離原則(SoC)。你可能非常想在一個你的代碼不同的地方定義事件處理器:你定義對事件的?*響應*(Promise鏈)的地方。如果沒有幫助機制,在這種模式下這么做很尷尬。
注意:?這種限制的另一種表述方法是,如果我們能夠構建某種能在它上面進行Promise鏈監聽的“可監聽對象(observable)”就好了。有一些庫已經建立這些抽象(比如RxJS——http://rxjs.codeplex.com/ ),但是這種抽象看起來是如此的重,以至于你甚至再也看不到Promise的性質。這樣的重抽象帶來一個重要的問題:這些機制是否像Promise本身被設計的一樣*可靠*。我們將會在附錄B中重新討論“觀察者(Observable)”模式。
### 惰性
對于在你的代碼中使用Promise而言一個實在的壁壘是,現存的所有代碼都沒有支持Promise。如果你有許多基于回調的代碼,讓代碼保持相同的風格容易多了。
“一段基于動作(用回調)的代碼將仍然基于動作(用回調),除非一個更聰明,具有Promise意識的開發者對它采取行動。”
Promise提供了一種不同的模式規范,如此,代碼的表達方式可能會變得有一點兒不同,某些情況下,則根本不同。你不得不有意這么做,因為Promise不僅只是把那些為你服務至今的老式編碼方法自然地抖落掉。
考慮一個像這樣的基于回調的場景:
```source-js
function foo(x,y,cb) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
cb
);
}
foo( 11, 31, function(err,text) {
if (err) {
console.error( err );
}
else {
console.log( text );
}
} );
```
將這個基于回調的代碼轉換為支持Promise的代碼的第一步該怎么做,是立即明確的嗎?這要看你的經驗。你練習的越多,它就感覺越自然。但當然,Promise沒有明確告知到底怎么做——沒有一個放之四海而皆準的答案——所以這要靠你的責任心。
就像我們以前講過的,我們絕對需要一種支持Promise的Ajax工具來取代基于回調的工具,我們可以稱它為`request(..)`。你可以制造自己的,正如我們已經做過的。但是不得不為每個基于回調的工具手動定義Promise相關的包裝器的負擔,使得你根本就不太可能選擇將代碼重構為Promise相關的。
Promise沒有為這種限制提供直接的答案。但是大多數Promise庫確實提供了幫助函數。想象一個這樣的幫助函數:
```source-js
// 填補的安全檢查
if (!Promise.wrap) {
Promise.wrap = function(fn) {
return function() {
var args = [].slice.call( arguments );
return new Promise( function(resolve,reject){
fn.apply(
null,
args.concat( function(err,v){
if (err) {
reject( err );
}
else {
resolve( v );
}
} )
);
} );
};
};
}
```
好吧,這可不是一個微不足道的工具。然而,雖然他可能看起來有點兒令人生畏,但也沒有你想的那么糟。它接收一個函數,這個函數期望一個錯誤優先風格的回調作為第一個參數,然后返回一個可以自動創建Promise并返回的新函數,然后為你替換掉回調,與Promise的完成/拒絕連接在一起。
與其浪費太多時間談論這個`Promise.wrap(..)`幫助函數?*如何*?工作,還不如讓我們來看看如何使用它:
```source-js
var request = Promise.wrap( ajax );
request( "http://some.url.1/" )
.then( .. )
..
```
哇哦,真簡單!
`Promise.wrap(..)`?不會?生產Promise。它生產一個將會生產Promise的函數。某種意義上,一個Promise生產函數可以被看做一個“Promise工廠”。我提議將這樣的東西命名為“promisory”("Promise" + "factory")。
這種將期望回調的函數包裝為一個Promise相關的函數的行為,有時被稱為“提升(lifting)”或“promise化(promisifying)”。但是除了“提升過的函數”以外,看起來沒有一個標準的名詞來稱呼這個結果函數,所以我更喜歡“promisory”,因為我認為他更具描述性。
注意:?Promisory不是一個瞎編的詞。它是一個真實存在的詞匯,而且它的定義是含有或載有一個promise。這正是這些函數所做的,所以這個術語匹配得簡直完美!
那么,`Promise.wrap(ajax)`生產了一個我們稱為`request(..)`的`ajax(..)`promisory,而這個promisory為Ajax應答生產Promise。
如果所有的函數已經都是promisory,我們就不需要自己制造它們,所以額外的步驟就有點兒多余。但是至少包裝模式是(通常都是)可重復的,所以我們可以把它放進`Promise.wrap(..)`幫助函數中來支援我們的promise編碼。
那么回到剛才的例子,我們需要為`ajax(..)`和`foo(..)`都做一個promisory。
```source-js
// 為`ajax(..)`制造一個promisory
var request = Promise.wrap( ajax );
// 重構`foo(..)`,但是為了代碼其他部分
// 的兼容性暫且保持它對外是基于回調的
// ——僅在內部使用`request(..)`'的promise
function foo(x,y,cb) {
request(
"http://some.url.1/?x=" + x + "&y=" + y
)
.then(
function fulfilled(text){
cb( null, text );
},
cb
);
}
// 現在,為了這段代碼本來的目的,為`foo(..)`制造一個promisory
var betterFoo = Promise.wrap( foo );
// 并使用這個promisory
betterFoo( 11, 31 )
.then(
function fulfilled(text){
console.log( text );
},
function rejected(err){
console.error( err );
}
);
```
當然,雖然我們將`foo(..)`重構為使用我們的新`request(..)`promisory,我們可以將`foo(..)`本身制成promisory,而不是保留基于會掉的實現并需要制造和使用后續的`betterFoo(..)`promisory。這個決定只是要看`foo(..)`是否需要保持基于回調的形式以便于代碼的其他部分兼容。
考慮這段代碼:
```source-js
// 現在,`foo(..)`也是一個promisory
// 因為它委托到`request(..)` promisory
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
foo( 11, 31 )
.then( .. )
..
```
雖然ES6的Promise沒有為這樣的promisory包裝提供原生的幫助函數,但是大多數庫提供它們,或者你可以制造自己的。不管哪種方法,這種Promise特定的限制是可以不費太多勁兒就可以解決的(當然是和回調地獄的痛苦相比!)。
### Promise不可撤銷
一旦你創建了一個Promise并給它注冊了一個完成和/或拒絕處理器,就沒有什么你可以從外部做的事情能停止這個進程,即使是某些其他的事情使這個任務變得毫無意義。
注意:?許多Promise抽象庫都提供取消Promise的功能,但這是一個非常壞的主意!許多開發者都希望Promise被原生地設計為具有外部取消能力,但問題是這將允許Promise的一個消費者/監聽器影響某些其他消費者監聽同一個Promise的能力。這違反了未來值得可靠性原則(外部不可變),另外就是嵌入了“遠距離行為(action at a distance)”的反模式。不管它看起來多么有用,它實際上會直接將你引回與回調地獄相同的噩夢。
考慮我們早先的Promise超時場景:
```source-js
var p = foo( 42 );
Promise.race( [
p,
timeoutPromise( 3000 )
] )
.then(
doSomething,
handleError
);
p.then( function(){
// 即使是在超時的情況下也會發生 :(
} );
```
“超時”對于promise`p`來說是外部的,所以`p`本身繼續運行,這可能不是我們想要的。
一個選項是侵入性地定義你的解析回調:
```source-js
var OK = true;
var p = foo( 42 );
Promise.race( [
p,
timeoutPromise( 3000 )
.catch( function(err){
OK = false;
throw err;
} )
] )
.then(
doSomething,
handleError
);
p.then( function(){
if (OK) {
// 僅在沒有超時的情況下發生! :)
}
} );
```
很難看。這可以工作,但是遠不理想。一般來說,你應當避免這樣的場景。
但是如果你不能,這種解決方案的丑陋應當是一個線索,說明?*取消*?是一種屬于在Promise之上的更高層抽象的功能。我推薦你找一個Promise抽象庫來輔助你,而不是自己使用黑科技。
注意:?我的?*asynquence*?Promise抽象庫提供了這樣的抽象,還為序列提供了一個`abort()`能力,這一切將在附錄A中討論。
一個單獨的Promise不是真正的流程控制機制(至少沒有多大實際意義),而流程控制機制正是?*取消*?要表達的;這就是為什么Promise取消顯得尷尬。
相比之下,一個鏈條的Promise集合在一起——我稱之為“序列”——?*是*?一個流程控制的表達,如此在這一層面的抽象上它就適于定義取消。
沒有一個單獨的Promise應該是可以取消的,但是一個?*序列*?可以取消是有道理的,因為你不會將一個序列作為一個不可變值傳來傳去,就像Promise那樣。
### Promise性能
這種限制既簡單又復雜。
比較一下在基于回調的異步任務鏈和Promise鏈上有多少東西在動,很明顯Promise有多得多的事情發生,這意味著它們自然地會更慢一點點。回想一下Promise提供的保證信任的簡單列表,將它和你為了達到相同保護效果而在回調上面添加的特殊代碼比較一下。
更多工作要做,更多的安全要保護,意味著Promise與赤裸裸的,不可靠的回調相比?*確實*?更慢。這些都很明顯,可能很容易縈繞在你腦海中。
但是慢多少?好吧……這實際上是一個難到不可思議的問題,無法絕對,全面地回答。
坦白地說,這是一個比較蘋果和橘子的問題,所以可能是問錯了。你實際上應當比較的是,帶有所有手動保護層的經過特殊處理的回調系統,是否比一個Promise實現要快。
如果說Promise有一種合理的性能限制,那就是它并不將可靠性保護的選項羅列出來讓你選擇——你總是一下得到全部。
如果我們承認Promise一般來說要比它的非Promise,不可靠的回調等價物?*慢一點兒*——假定在有些地方你覺得你可以自己調整可靠性的缺失——難道這意味著Promise應當被全面地避免,就好像你的整個應用程序僅僅由一些可能的“必須絕對最快”的代碼驅動著?
捫心自問:如果你的代碼有那么合理,那么?對于這樣的任務,JavaScript是正確的選擇嗎??為了運行應用程序JavaScript可以被優化得十分高效(參見第五章和第六章)。但是在Promise提供的所有好處的光輝之下,過于沉迷它微小的性能權衡,*真的*?合適嗎?
另一個微妙的問題是Promise使?*所有事情*?都成為異步的,這意味著有些應當立即完成的(同步的)步驟也要推遲到下一個Job步驟中(參見第一章)。也就是說一個Promise任務序列要比使用回調連接的相同序列要完成的稍微慢一些是可能的。
當然,這里的問題是:這些關于性能的微小零頭的潛在疏忽,和我們在本章通篇闡述的Promise帶來的益處相比,*還值得考慮嗎?*
我的觀點是,在幾乎所有你可能認為Promise的性能慢到了需要被考慮的情況下,完全回避Promise并將它的可靠性和組合性優化掉,實際上是一種反模式。
相反地,你應當默認地在代碼中廣泛使用它們,然后再記錄并分析你的應用程序的熱(關鍵)路徑。Promise?*真的*?是瓶頸?還是它們只是理論上慢了下來?只有在那?*之后*,拿著實際合法的基準分析觀測數據(參見第六章),再將Promise從這些關鍵區域中重構移除才稱得上是合理與謹慎。
Promise是有一點兒慢,但作為交換你得到了很多內建的可靠性,無Zalgo的可預測性,與組合性。也許真正的限制不是它們的性能,而是你對它們的益處缺乏認識?
## 復習
Promise很牛。用它們。它們解決了肆虐在回調代碼中的?*控制倒轉*?問題。
它們沒有擺脫回調,而是重新定向了這些回調的組織安排方式,是它成為一種坐落于我們和其他工具之間的可靠的中間機制。
Promise鏈還開始以順序的風格定義了一種更好的(當然,還不完美)表達異步流程的方式,它幫我們的大腦更好的規劃和維護異步JS代碼。我們會在下一章中看到一個更好的解決?*這個*?問題的方法!