異步 JavaScript 與 Promise

異步?

我在不少地方都看到過異步(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的異步難題

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

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

// Ajax請求示意代碼
$.ajax({
    url: url,
    data: dataObject,
    success: function(){},
    error: function(){}
});

這樣的寫法有什麼問題嗎?簡單來講,不夠輕便。爲何必定要在發起請求的地方,就要把successerror這些回調給寫好呢?假如個人回調要作不少不少的事情,是要我想起一件事情就跑回這裏添加代碼嗎?線程

再好比,咱們要完成這樣一件事:有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://acgtofe.com/posts/2015...

相關文章
相關標籤/搜索