CommonJS Promises/A規範

本文來自四火哥的翻譯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對象,它包含兩種狀況:

  • 被某個數據裝載(fulfill)
  • 被某個異常的拋出中斷了

若是你正確遵守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是怎樣被建立的:

  • 若是handler返回了一個值,那麼新的promise就要裝載那個值。
  • 若是handler拋出異常,那麼新的promise就要用一個異常來表示拒絕繼續日後執行。

咱們根據promise的不一樣狀態把這個場景分解一下,就能夠知道爲何這幾句話那麼重要了:

  • 數據裝填完成,fulfillment handler返回了一個值值:簡單的函數轉換
  • 數據裝填完成,可是fulfillment handler拋出了異常:獲取數據,而後再拋出異常
  • 數據裝填失敗,rejection handler返回了一個值:必須得用一個catch子句捕獲異常並處理
  • 數據裝填失敗,可是rejection handler拋出了異常:必須得用一個catch子句捕獲並從新拋出(能夠從新拋出一個新的異常)

若是沒有這些,你就失去了同步/異步並行處理的威力,那麼你的所謂的「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+標準的,我如今毫無保留地推薦給你:

  • Q:Kris Kowal和我寫的,一個promise特性徹底實現的類庫,有豐富的API、Node.js的支持、處理流支持,以及初步的對於長堆棧的支持。
  • RSVP.js:Yehuda Katz寫的,很是輕量的promise的徹底實現。
  • when.js:Brian Cavalier寫的,一個任務管理的中間庫,能夠部署和取消任務執行。

若是你對使用JQuery殘廢的promise感到不爽,我推薦你使用上面類庫的工具方法來實現你一樣的目的(通常都是一個叫作when的方法),把這個殘廢的promise對象變成一個健全的promise對象:

1
2
var promise = Q.when($.get( "https://github.com/kriskowal/q" ));
// aaaah, much better
相關文章
相關標籤/搜索