異步JavaScript與Promise

異步?

我在不少地方都看到過 異步(Asynchronous)這個詞,但在我還不是很理解這個概念的時候,卻發現本身經常會被當作「已經很清楚」(* ̄ロ ̄)。

若是你也有相似的狀況,不要緊,搜索一下這個詞,就能夠獲得大體的說明。在這裏,我會對JavaScript的異步作一點額外解釋。

看一下這段代碼:

JavaScript代碼
  1. var start = new Date();
  2. setTimeout(function(){
  3.   var end = new Date();
  4.   console.log("Time elapsed: ", end - start, "ms");
  5. }, 500);
  6. while (new Date - start < 1000) {};
這段代碼運行後會獲得相似Time elapsed: 1013ms這樣的結果。 setTimeout()所設定的在將來500ms時執行的函數,實際等了比1000ms更多的時間後才執行。

要如何解釋呢?調用setTimeout()時,一個延時事件被排入隊列。而後,繼續執行這以後的代碼,以及更後邊的代碼,直到沒有任何代碼。沒有任何代 碼後,JavaScript線程進入空閒,此時JavaScript執行引擎纔去翻看隊列,在隊列中找到「應該觸發」的事件,而後調用這個事件的處理器 (函數)。處理器執行完成後,又再返回到隊列,而後查看下一個事件。

單線程的JavaScript,就是這樣經過隊列,以事件循環的形式工做的。因此,前面的代碼中,是用while將執行引擎拖在代碼運行期間長達1000ms,而在所有代碼運行完回到隊列前,任何事件都不會觸發。這就是JavaScript的異步機制。

JavaScript的異步難題

JavaScript中的異步操做可能不老是簡單易行的。

Ajax也許是咱們用得最多的異步操做。以jQuery爲例,發起一個Ajax請求的代碼通常是這樣的:

JavaScript代碼
  1. // Ajax請求示意代碼
  2. $.ajax({
  3.   url: url,
  4.   data: dataObject,
  5.   success: function(){},
  6.   error: function(){}
  7. });
這樣的寫法有什麼問題嗎?簡單來講,不夠輕便。爲何必定要在發起請求的地方,就要把success和error這些回調給寫好呢?假如個人回調要作不少不少的事情,是要我想起一件事情就跑回這裏添加代碼嗎?

再好比,咱們要完成這樣一件事:有4個供Ajax訪問的url地址,須要先Ajax訪問第1個,在第1個訪問完成後,用拿到的返回數據做爲參數再訪問第2個,第2個訪問完成後再第3個...以此到4個所有訪問完成。按照這樣的寫法,彷佛會變成這樣:

JavaScript代碼
  1. $.ajax({
  2.   url: url1,
  3.   success: function(data){
  4.     $.ajax({
  5.     url: url2,
  6.     data: data,
  7.     success: function(data){
  8.       $.ajax({
  9.         //...
  10.       });
  11.     }  
  12.     });
  13.   }
  14. })
你必定會以爲這種稱爲Pyramid of Doom(金字塔厄運)的代碼看起來很糟糕。習慣了直接附加回調的寫法,就可能會對這種一個傳遞到下一個的異步事件感到無從入手。爲這些回調函數分別命名並分離存放能夠在形式上減小嵌套,使代碼清晰,但仍然不能解決問題。

另外一個常見的難點是,同時發送兩個Ajax請求,而後要在兩個請求都成功返回後再作一件接下來的事,想想若是隻按前面的方式在各自的調用位置去附加回調,這是否是好像也有點難辦?

適於應對這些異步操做,可讓你寫出更優雅代碼的就是Promise。

Promise上場

Promise是什麼呢?先繼續之前面jQuery的Ajax請求示意代碼爲例,那段代碼其實能夠寫成這個樣子:
JavaScript代碼
  1. var promise = $.ajax({
  2.   url: url,
  3.   data: dataObject
  4. });
  5. promise.done(function(){});
  6. promise.fail(function(){});
這和前面的Ajax請求示意代碼是等效的。能夠看到,Promise的加入使得代碼形式發生了變化。Ajax請求就好像變量賦值同樣,被「保存」了起來。這就是封裝,封裝將真正意義上讓異步事件變得容易起來。

封裝是有用的Promise對象就像是一個封裝好的對異步事件的引用。想要在這個異步事件完成後作點事情?給它附加回調就能夠了,無論附加多少個也沒問題!

jQuery的Ajax方法會返回一個Promise對象(這是jQuery1.5重點增長的特性)。若是我有do1()、do2()兩個函數要在異步事件成功完成後執行,只須要這樣作:

JavaScript代碼
  1. promise.done(do1);
  2. // Other code here.
  3. promise.done(do2);
這樣可要自由多了,我只要保存這個Promise對象,就在寫代碼的任什麼時候候,給它附加任意數量的回調,而不用管這個異步事件是在哪裏發起的。這就是Promise的優點。

正式的介紹

Promise應對異步操做是如此有用,以致於發展爲了CommonJS的一個規範,叫作 Promises/A。Promise表明的是某一操做結束後的返回值,它有3種狀態:

  • 確定(fulfilled或resolved),代表該Promise的操做成功了。
  • 否認(rejected或failed),代表該Promise的操做失敗了。
  • 等待(pending),尚未獲得確定或者否認的結果,進行中。
此外,還有1種名義上的狀態用來表示Promise的操做已經成功或失敗,也就是確定和否認狀態的集合,叫作 結束(settled)。Promise還具備如下重要的特性:
  • 一個Promise只能從等待狀態轉變爲確定或否認狀態一次,一旦轉變爲確定或否認狀態,就不再會改變狀態。
  • 若是在一個Promise結束(成功或失敗,同前面的說明)後,添加針對成功或失敗的回調,則回調函數會當即執行。
想一想Ajax操做,發起一個請求後,等待着,而後成功收到返回或出現錯誤(失敗)。這是否和Promise至關一致?

進一步解釋Promise的特性還有一個很好的例子:jQuery的$(document).ready(onReady)。其中onReady回調函數 會在DOM就緒後執行,但有趣的是,若是在執行到這句代碼以前,DOM就已經就緒了,那麼onReady會當即執行,沒有任何延遲(也就是說,是同步 的)。

Promise示例

生成Promise

Promises/A裏列出了一系列實現了Promise的JavaScript庫,jQuery也在其中。下面是用jQuery生成Promise的代碼:

JavaScript代碼
  1. var deferred = $.Deferred();
  2. deferred.done(function(message){console.log("Done: " + message)});
  3. deferred.resolve("morin");  // Done: morin
jQuery 本身特地定義了名爲Deferred的類,它實際上就是Promise。$.Deferred()方法會返回一個新生成的Promise實例。一方面,使 用deferred.done()、deferred.fail()等爲它附加回調,另外一方面,調用deferred.resolve()或 deferred.reject()來確定或否認這個Promise,且能夠向回調傳遞任意數據。

合併Promise

還記得我前文說的同時發送2個Ajax請求的難題嗎?繼續以jQuery爲例,Promise將能夠這樣解決它:

JavaScript代碼
  1. var promise1 = $.ajax(url1),
  2. promise2 = $.ajax(url2),
  3. promiseCombined = $.when(promise1, promise2);
  4. promiseCombined.done(onDone);
$.when() 方法能夠合併多個Promise獲得一個新的Promise,至關於在原多個Promise之間創建了AND(邏輯與)的關係,若是全部組成 Promise都已成功,則令合併後的Promise也成功,若是有任意一個組成Promise失敗,則當即令合併後的Promise失敗。

級聯Promise

再繼續我前文的依次執行一系列異步任務的問題。它將用到Promise最爲重要的.then()方法(在Promises/A規範中,也是用「有then()方法的對象」來定義Promise的)。代碼以下:

JavaScript代碼
  1. var promise = $.ajax(url1);
  2. promise = promise.then(function(data){
  3.   return $.ajax(url2, data);
  4. });
  5. promise = promise.then(function(data){
  6.   return $.ajax(url3, data);
  7. });
  8. // ...
Promise 的.then()方法的完整形式是.then(onDone, onFail, onProgress),這樣看上去,它像是一個一次性就能夠把各類回調都附加上去的簡便方法(.done()、.fail()能夠不用了)。沒錯,你的 確能夠這樣使用,這是等效的。

但.then()方法還有它更爲有用的功能。如同then這個單詞自己的意義那樣,它用來清晰地指明異步事件的先後關係:「先這個,而後(then)再那個」。這稱爲Promise的級聯。

要級聯Promise,須要注意的是,在傳遞給then()的回調函數中,必定要返回你想要的表明下一步任務的Promise(如上面代碼 的$.ajax(url2, data))。這樣,前面被賦值的那個變量纔會變成新的Promise。而若是then()的回調函數返回的不是Promise,則then()方法會返 回最初的那個Promise。

應該會以爲有些難理解?從代碼執行的角度上說,上面這段帶有多個then()的代碼其實仍是被JavaScript引擎運行一遍就結束。但它就像是寫好的 舞臺劇的劇本同樣,讀過一遍後,JavaScript引擎就會在將來的時刻,依次安排演員按照劇原本演出,而演出都是異步的。then()方法就是讓你能 寫出異步劇本的筆。

將Promise用在基於回調函數的API

前文反覆用到的$.ajax()方法會返回一個Promise對象,這其實只是jQuery特地提供的福利。實際狀況是,大多數JavaScript API,包括Node.js中的原生函數,都基於回調函數,而不是基於Promise。這種狀況下使用Promise會須要自行作一些加工。

這個加工其實比較簡單和直接,下面是例子:
JavaScript代碼
  1. var deferred = $.Deferred();
  2. setTimeout(deferred.resolve, 1000);
  3. deferred.done(onDone);
這樣,將Promise的確定或否認的觸發器,做爲API的回調傳入,就變成了Promise的處理模式了。

Promise是怎麼實現出來的?

本文寫Promise寫到這裏,你發現了全都是基於已有的實現了Promise的庫。那麼,若是要自行構築一個Promise的話呢?

位列於 Promises/A的庫列表第一位的 Q能夠算是最符合Promises/A規範且至關直觀的實現。若是你想了解如何作出一個Promise,能夠參考Q提供的 設計模式解析

限於篇幅,本文只介紹Promise的應用。我會在之後單獨開一篇文章來詳述Promise的實現細節。

做爲JavaScript後續版本的ECMAScript 6將原生提供Promise,若是你想知道它的用法,推薦閱讀 JavaScript Promises: There and back again

結語

Promise這個詞頑強到不適合翻譯,一眼之下都會以爲意義不明。不過,在JavaScript裏作比較複雜的異步任務時,它的確能夠提供至關多的幫助。


原文: http://segmentfault.com/blog/yardtea/1190000002526897
相關文章
相關標籤/搜索