異步?
我在不少地方都看到過
異步(Asynchronous)這個詞,但在我還不是很理解這個概念的時候,卻發現本身經常會被當作「已經很清楚」(* ̄ロ ̄)。
若是你也有相似的狀況,不要緊,搜索一下這個詞,就能夠獲得大體的說明。在這裏,我會對JavaScript的異步作一點額外解釋。
看一下這段代碼:
- var start = new Date();
- setTimeout(function(){
- var end = new Date();
- console.log("Time elapsed: ", end - start, "ms");
- }, 500);
- while (new Date - start < 1000) {};
這段代碼運行後會獲得相似Time elapsed: 1013ms這樣的結果。 setTimeout()所設定的在將來500ms時執行的函數,實際等了比1000ms更多的時間後才執行。
要如何解釋呢?調用setTimeout()時,一個延時事件被排入隊列。而後,繼續執行這以後的代碼,以及更後邊的代碼,直到沒有任何代碼。沒有任何代 碼後,JavaScript線程進入空閒,此時JavaScript執行引擎纔去翻看隊列,在隊列中找到「應該觸發」的事件,而後調用這個事件的處理器 (函數)。處理器執行完成後,又再返回到隊列,而後查看下一個事件。
單線程的JavaScript,就是這樣經過隊列,以事件循環的形式工做的。因此,前面的代碼中,是用while將執行引擎拖在代碼運行期間長達1000ms,而在所有代碼運行完回到隊列前,任何事件都不會觸發。這就是JavaScript的異步機制。
JavaScript的異步難題
JavaScript中的異步操做可能不老是簡單易行的。
Ajax也許是咱們用得最多的異步操做。以jQuery爲例,發起一個Ajax請求的代碼通常是這樣的:
- // Ajax請求示意代碼
- $.ajax({
- url: url,
- data: dataObject,
- success: function(){},
- error: function(){}
- });
這樣的寫法有什麼問題嗎?簡單來講,不夠輕便。爲何必定要在發起請求的地方,就要把success和error這些回調給寫好呢?假如個人回調要作不少不少的事情,是要我想起一件事情就跑回這裏添加代碼嗎?
再好比,咱們要完成這樣一件事:有4個供Ajax訪問的url地址,須要先Ajax訪問第1個,在第1個訪問完成後,用拿到的返回數據做爲參數再訪問第2個,第2個訪問完成後再第3個...以此到4個所有訪問完成。按照這樣的寫法,彷佛會變成這樣:
- $.ajax({
- url: url1,
- success: function(data){
- $.ajax({
- url: url2,
- data: data,
- success: function(data){
- $.ajax({
- //...
- });
- }
- });
- }
- })
你必定會以爲這種稱爲Pyramid of Doom(金字塔厄運)的代碼看起來很糟糕。習慣了直接附加回調的寫法,就可能會對這種一個傳遞到下一個的異步事件感到無從入手。爲這些回調函數分別命名並分離存放能夠在形式上減小嵌套,使代碼清晰,但仍然不能解決問題。
另外一個常見的難點是,同時發送兩個Ajax請求,而後要在兩個請求都成功返回後再作一件接下來的事,想想若是隻按前面的方式在各自的調用位置去附加回調,這是否是好像也有點難辦?
適於應對這些異步操做,可讓你寫出更優雅代碼的就是Promise。
Promise上場
Promise是什麼呢?先繼續之前面jQuery的Ajax請求示意代碼爲例,那段代碼其實能夠寫成這個樣子:
- var promise = $.ajax({
- url: url,
- data: dataObject
- });
- promise.done(function(){});
- promise.fail(function(){});
這和前面的Ajax請求示意代碼是等效的。能夠看到,Promise的加入使得代碼形式發生了變化。Ajax請求就好像變量賦值同樣,被「保存」了起來。這就是封裝,封裝將真正意義上讓異步事件變得容易起來。
封裝是有用的Promise對象就像是一個封裝好的對異步事件的引用。想要在這個異步事件完成後作點事情?給它附加回調就能夠了,無論附加多少個也沒問題!
jQuery的Ajax方法會返回一個Promise對象(這是jQuery1.5重點增長的特性)。若是我有do1()、do2()兩個函數要在異步事件成功完成後執行,只須要這樣作:
- promise.done(do1);
- // Other code here.
- 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的代碼:
- var deferred = $.Deferred();
- deferred.done(function(message){console.log("Done: " + message)});
- deferred.resolve("morin"); // Done: morin
jQuery 本身特地定義了名爲Deferred的類,它實際上就是Promise。$.Deferred()方法會返回一個新生成的Promise實例。一方面,使 用deferred.done()、deferred.fail()等爲它附加回調,另外一方面,調用deferred.resolve()或 deferred.reject()來確定或否認這個Promise,且能夠向回調傳遞任意數據。
合併Promise
還記得我前文說的同時發送2個Ajax請求的難題嗎?繼續以jQuery爲例,Promise將能夠這樣解決它:
- var promise1 = $.ajax(url1),
- promise2 = $.ajax(url2),
- promiseCombined = $.when(promise1, promise2);
- promiseCombined.done(onDone);
$.when() 方法能夠合併多個Promise獲得一個新的Promise,至關於在原多個Promise之間創建了AND(邏輯與)的關係,若是全部組成 Promise都已成功,則令合併後的Promise也成功,若是有任意一個組成Promise失敗,則當即令合併後的Promise失敗。
級聯Promise
再繼續我前文的依次執行一系列異步任務的問題。它將用到Promise最爲重要的.then()方法(在Promises/A規範中,也是用「有then()方法的對象」來定義Promise的)。代碼以下:
- var promise = $.ajax(url1);
- promise = promise.then(function(data){
- return $.ajax(url2, data);
- });
- promise = promise.then(function(data){
- return $.ajax(url3, data);
- });
- // ...
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會須要自行作一些加工。
這個加工其實比較簡單和直接,下面是例子:
- var deferred = $.Deferred();
- setTimeout(deferred.resolve, 1000);
- 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