我在不少地方都看到過異步(Asynchronous)這個詞,但在我還不是很理解這個概念的時候,卻發現本身經常會被當作「已經很清楚」(* ̄ロ ̄)。ajax
若是你也有相似的狀況,不要緊,搜索一下這個詞,就能夠獲得大體的說明。在這裏,我會對JavaScript的異步作一點額外解釋。設計模式
看一下這段代碼:promise
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執行引擎纔去翻看隊列,在隊列中找到「應該觸發」的事件,而後調用這個事件的處理器(函數)。處理器執行完成後,又再返回到隊列,而後查看下一個事件。async
單線程的JavaScript,就是這樣經過隊列,以事件循環的形式工做的。因此,前面的代碼中,是用while
將執行引擎拖在代碼運行期間長達1000ms,而在所有代碼運行完回到隊列前,任何事件都不會觸發。這就是JavaScript的異步機制。函數
JavaScript中的異步操做可能不老是簡單易行的。post
Ajax也許是咱們用得最多的異步操做。以jQuery爲例,發起一個Ajax請求的代碼通常是這樣的:url
// 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是什麼呢?先繼續之前面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
會當即執行,沒有任何延遲(也就是說,是同步的)。
[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,且能夠向回調傳遞任意數據。
還記得我前文說的同時發送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最爲重要的.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()
方法就是讓你能寫出異步劇本的筆。
前文反覆用到的$.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的話呢?
位列於[Promises/A][]的庫列表第一位的[Q][]能夠算是最符合Promises/A規範且至關直觀的實現。若是你想了解如何作出一個Promise,能夠參考Q提供的[設計模式解析][]。
限於篇幅,本文只介紹Promise的應用。我會在之後單獨開一篇文章來詳述Promise的實現細節。
做爲JavaScript後續版本的ECMAScript 6將原生提供Promise,若是你想知道它的用法,推薦閱讀[JavaScript Promises: There and back again][]。
Promise這個詞頑強到不適合翻譯,一眼之下都會以爲意義不明。不過,在JavaScript裏作比較複雜的異步任務時,它的確能夠提供至關多的幫助。
(從新編輯自個人博客,原文地址:http://acgtofe.com/posts/2015...)