本文來自四火哥的翻譯javascript
CommonJS是一組javascript編程規範,而promise是其中之一。html
簡而言之,promises是一種令代碼的異步行爲變得更加優雅的軟件抽象。在基本的定義中,代碼可能一直是這樣寫的java
getTweetsFor("domenic", function (err, results) { // the rest of your code goes here. });
如今你的方法有一個返回值,叫作promise,它表明操做的最終結果。node
var promiseForTweets = getTweets("domenic");
這是很重要的由於你能夠把promiseForTweets看成一等對象,傳值給他們,聚合他們等等,而不是搞一堆耦合在一塊兒的回調函數去完成你邏輯。git
我曾經說過我認爲promises有多酷,這裏就再也不贅述。相反,我如今要說的是我看到一個不安的趨勢關於最近javascript類庫已經加入promise支持,他們徹底忽略了promise的關鍵點。github
Then方法和CommonJS Promise/A規範編程
若是有人說promise是JavaScript的上下文,那麼他至少指的是CommonJS的Promises/A規範。這大概是我見過的最簡陋的規範了,基本上只是對於這一類函數的行爲作了簡單說明:api
promise是一種以函數來做爲then屬性值的對象:數組
then(fulfilledHandler, errorHandler, progressHandler)promise
添加fulfilledHandler、errorHandler和progressHandler後,promise對象就構成了。fulfilledHandler是在promise被裝載數據的時候調用,errorHandler在promise失敗的時候調用,progressHandler則在progress事件觸發的時候調用。全部的參數都是可選的,而且非function的參數都會被忽略掉。有時progressHandler並不僅是一個可選參數,可是progress事件確是純粹的可選參數而已。promise模式的實現者並不必定要每次都調用progressHandler(由於它能夠被忽略掉),只有這個參數傳入的時候纔會發生調用。
這個方法在fulfilledHandler或者errorHandler回調完成以後,得返回一個新的promise對象。這樣一來,promise操做就能夠造成鏈式調用。回調handler的返回值是一個promise對象。若是回調拋出異常,這個返回的promise對象就會把狀態設爲失敗。
人們通常都理解第一段話,基本上能夠歸結爲回調函數的聚合。
經過then方法來關聯起回調函數和promise對象,不論是成功、失敗仍是進行中。當promise對象改變狀態時(這超出了這篇短小文檔討論的範圍),回調函數會被執行,我以爲這頗有用。
可是人們不怎麼理解的第二段,偏偏是最重要的。
那麼Promises的要點是啥?
最重要的是,promises根本就不是簡單的回調函數聚合。promises並非那麼簡單的東西,它是一種爲同步函數和異步函數提供直接一致性的模式。
啥意思呢?咱們先來看同步函數兩個很是重要的特性:
這兩個都是必不可少的。你能夠把一個函數的返回值做爲參數傳給下一個函數,再把下一個函數的返回值做爲參數傳給下下個,一直重複下去。如今,若是中間出現失敗的狀況,那個函數的鏈會拋出異常,異常會向上傳播,直到有人能夠來處理它爲止。
在異步編程的世界裏,你無法「返回」一個值了,它無法被及時地讀取到。類似的,你也無法拋出異常了,由於沒有人回去捕獲它。因此咱們踏入了「回調的地獄」,返回值嵌套了回調,錯誤須要手動傳給原有的調用鏈,這樣你就得引入相似於像domain這樣瘋狂的東西了。
下面四火對domain作一個小的說明:
異步編程中,你無法簡單地經過try-catch來處理異常:
1
2
3
4
5
6
7
|
try
{
process.nextTick(
function
() {
// do something
});
}
catch
(err) {
//you can not catch it
}
|
因此Node.js給的使用domain的解決方法是:
1
2
3
4
5
|
var
doo = domain.create();
// listen to error event
doo.on(
'error'
,
function
(err) {
// you got an error
});
|
固然,這個方法並不完美,仍是會存在堆棧丟失等問題。
promises如今須要給咱們異步世界裏的函數組成和錯誤冒泡機制。如今假使你的函數要返回一個promise對象,它包含兩種狀況:
若是你正確遵守Promises/A規範實現,fulfillment或者rejection部分的代碼就像同步代碼的副本同樣,在整個調用鏈中,fulfillment部分會執行,也會在某個時候被rejection中斷,可是隻有預先聲明瞭的handler才能處理它。
換言之,下面這段代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
getTweetsFor(
"domenic"
)
// promise-returning function
.then(
function
(tweets) {
var
shortUrls = parseTweetsForUrls(tweets);
var
mostRecentShortUrl = shortUrls[0];
return
expandUrlUsingTwitterApi(mostRecentShortUrl);
// promise-returning function
})
.then(httpGet)
// promise-returning function
.then(
function
(responseBody) {
console.log(
"Most recent link text:"
, responseBody);
},
function
(error) {
console.error(
"Error with the twitterverse:"
, error);
}
);
|
至關於這樣的同步代碼:
1
2
3
4
5
6
7
8
9
|
try
{
var
tweets = getTweetsFor(
"domenic"
);
// blocking
var
shortUrls = parseTweetsForUrls(tweets);
var
mostRecentShortUrl = shortUrls[0];
var
responseBody = httpGet(expandUrlUsingTwitterApi(mostRecentShortUrl));
// blocking x 2
console.log(
"Most recent link text:"
, responseBody);
}
catch
(error) {
console.error(
"Error with the twitterverse: "
, error);
}
|
無論錯誤怎樣發生,都必需要有顯式的錯誤捕獲處理機制。在將要到來的ECMAScript 6的版本中,使用了一些內部技巧,大多數狀況下代碼仍是同樣的。
第二段話
第二段話實際上是徹底有必要的:
這個方法在fulfilledHandler或者errorHandler回調完成以後,得返回一個新的promise對象。這樣一來,promise操 做就能夠造成鏈式調用。回調handler的返回值是一個promise對象。若是回調拋出異常,這個返回的promise對象就會把狀態設爲失敗。
換言之,then方法並無一個機制去把一堆回調方法附着到某個集合中去,它的機制只不過是把原有對象轉換成promise對象,以及生成新的promise對象。
這就解釋了第一段的關鍵:函數應當返回一個新的promise對象。JQuery(1.8之前的版本)卻不這麼作。他們只是繼續使用原有的promise對象,可是把它的狀態改變一下而已。這就意味着若是你把promise對象給客戶了,他們實際上是能夠能夠改變它的狀態的。爲了說明這一點有多荒謬,你能夠想想一個同步的例子:若是你把一個函數的返回值給了兩我的,其中一個能夠改變一下返回值裏面的東西,而後這兩我的手裏的返回值竟然就拋出異常來了!事實上,Promises/A規範其實已經說明了這一點:
一旦promise裝載數據完成或者失敗了,promise的值就不能夠再改變了,就像JavaScript中的數值、原語類型、對象ID等等,都是不能夠被改變的。
如今考慮其中的最後兩句話,它們說出了promise是怎樣被建立的:
咱們根據promise的不一樣狀態把這個場景分解一下,就能夠知道爲何這幾句話那麼重要了:
若是沒有這些,你就失去了同步/異步並行處理的威力,那麼你的所謂的「promises」也就變成了簡單的回調函數聚合而已了。這也是JQuery當前對promises的實現的問題所在,它只實現了上面說的第一個場景而已。這也是Node.js 0.1中基於EventEmitter的promise的問題之一。
更進一步說,捕獲異常並轉換狀態,咱們須要處理預期和非預期的異常,這和寫同步代碼沒什麼區別。若是你在某個handler裏面寫一個叫作aFunctionThatDoesNotExist()的函數,你的promise對象失敗之後會拋出異常,接着你的異常向上冒泡,外面最近的一個rejection handler會處理它,這看起來就像你在那裏手寫了new Error("bad data")同樣。看吧,沒有domain。
那又如何
也許你如今被我這樣一波一波的解釋感到壓力陡增,想不明白爲何我會對那些寫出這些糟糕行爲的類庫那麼惱火。
如今我告訴你爲何:
promise對象是一個被定義爲擁有一個then方法的返回值的對象。
對於Promises/A規範實現類庫的做者,咱們必須作到:凡是寫出then方法這樣機制的promise,都得去徹底地符合Promises/A規範。
若是你也認爲這樣的話是對的,那麼你也能夠寫出這樣的擴展庫,不論是Q、when.js,或者是WinJS,你可使用Promises/A規範中最基本的規則定義,去構建promise的行爲。好比這個,一個能夠和一切真正知足Promises/A規範的類庫一塊兒工做的retry函數。
然而,不幸的是,像JQuery這樣的類庫卻破壞了這條守則,它迫使醜陋的hack代碼去檢測這些冒充promises的對象——雖然JQuery依然在API文檔裏面號稱這是「promise」對象:
1
2
3
|
if
(
typeof
assertion._obj.pipe ===
"function"
) {
throw
new
TypeError(
"Chai as Promised is incompatible with jQuery's so-called 「promises.」 Sorry!"
);
}
|
若是API的使用者堅持使用JQuery promises的話,你大概只有兩種選擇:在執行過程當中莫名其妙地、使人困惑地失敗,或者完全失敗,而且阻塞你繼續使用整個類庫。這可真糟糕啊。
繼續向前
這就是我爲何儘量地避免在Ember中使用回調函數聚合器了,這也是我寫這篇文章的緣由,並且,你能夠看一下我寫的這個準確兼容Promises/A規範的套件,這樣咱們就能夠在認識層面上達成一致了。
這個測試套件發佈之後,promise操做性和可理解性都有了進步。rsvp.js發佈的其中一個目標就是要提供對Promises/A的支持。不過最棒的是這個Promises/A+組織的開源項目,一個鬆耦合的實現,用清晰的和測試完備的方式呈現擴展了原有Promises/A規範,成爲Promises/A+規範。
固然,還有不少工做要作。值得注意的是,在寫這篇文章的時候,JQuery的最新版本是1.9.1,它的promises在錯誤處理上的實現是徹底錯誤的。我但願在接下去的JQuery 2.0版本中參考Promises/A+的文檔,修正這個問題。
同時,這些類庫是很是好地遵守Promises/A+標準的,我如今毫無保留地推薦給你:
若是你對使用JQuery殘廢的promise感到不爽,我推薦你使用上面類庫的工具方法來實現你一樣的目的(通常都是一個叫作when的方法),把這個殘廢的promise對象變成一個健全的promise對象:
1
2
|
// aaaah, much better
|