本章我們將會學習 Promise 提供的各種方法以及如何進行錯誤處理。
一般情況下我們都會使用 new Promise() 來創(chuàng)建 promise 對象,但是除此之外我們也可以使用其他方法。
在這里,我們將會學習如何使用 Promise.resolve 和 Promise.reject 這兩個方法。
靜態(tài)方法 Promise.resolve(value) 可以認為是 new Promise() 方法的快捷方式。
比如 Promise.resolve(42); 可以認為是以下代碼的語法糖。
new Promise(function(resolve){
resolve(42);
});
在這段代碼中的 resolve(42); 會讓這個promise對象立即進入確定(即 resolved)狀態(tài),并將 42 傳遞給后面 then 里所指定的 onFulfilled 函數(shù)。
方法 Promise.resolve(value); 的返回值也是一個 promise 對象,所以我們可以像下面那樣接著對其返回值進行 .then 調(diào)用。
Promise.resolve(42).then(function(value){
console.log(value);
});
Promise.resolve 作為 new Promise() 的快捷方式,在進行 promise 對象的初始化或者編寫測試代碼的時候都非常方便。
Promise.resolve 方法另一個作用就是將 thenable 對象轉(zhuǎn)換為 promise 對象。
ES6 Promises 里提到了 Thenable 這個概念,簡單來說它就是一個非常類似 promise 的東西。
就像我們有時稱具有 .length 方法的非數(shù)組對象為 Array like 一樣,thenable 指的是一個具有 .then 方法的對象。
這種將 thenable 對象轉(zhuǎn)換為 promise 對象的機制要求 thenable 對象所擁有的 then 方法應該和 Promise 所擁有的 then 方法具有同樣的功能和處理過程,在將 thenable 對象轉(zhuǎn)換為 promise 對象的時候,還會巧妙的利用 thenable 對象原來具有的 then 方法。
到底什么樣的對象能算是 thenable 的呢,最簡單的例子就是 jQuery.ajax(),它的返回值就是 thenable 的。
因為 jQuery.ajax() 的返回值是 jqXHR Objec 對象,這個對象具有 .then 方法。
$.ajax('/json/comment.json');// => 擁有 `.then` 方法的對象
這個 thenable 的對象可以使用 Promise.resolve 來轉(zhuǎn)換為一個promise對象。
變成了 promise 對象的話,就能直接使用 then 或者 catch 等這些在 ES6 Promises 里定義的方法了。
將 thenable 對象轉(zhuǎn)換 promise 對象
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise對象
promise.then(function(value){
console.log(value);
});
WARNING
jQuery 和 thenablejQuery.ajax() 的返回值是一個具有
.then方法的 jqXHR Object 對象,這個對象繼承了來自 Deferred Object 的方法和屬性。但是 Deferred Object 并沒有遵循 Promises/A+ 或 ES6 Promises 標準,所以即使看上去這個對象轉(zhuǎn)換成了一個 promise 對象,但是會出現(xiàn)缺失部分信息的問題。
這個問題的根源在于 jQuery 的 Deferred Object 的
then方法機制與 promise 不同。所以我們應該注意,即使一個對象具有
.then方法,也不一定就能作為 ES6 Promises 對象使用。
Promise.resolve 只使用了共通的方法 then ,提供了在不同的類庫之間進行 promise 對象互相轉(zhuǎn)換的功能。
這種轉(zhuǎn)換為 thenable 的功能在之前是通過使用 Promise.cast 來完成的,從它的名字我們也不難想象它的功能是什么。
除了在編寫使用 Promise 的類庫等軟件時需要對 Thenable 有所了解之外,通常作為 end-user 使用的時候,我們可能不會用到此功能。
我們會在后面第4章的 Promise.resolve 和 Thenable 中進行詳細的說明,介紹一下結(jié)合使用了 Thenable 和 Promise.resolve 的具體例子。
簡單總結(jié)一下 Promise.resolve 方法的話,可以認為它的作用就是將傳遞給它的參數(shù)填充(Fulfilled)到 promise 對象后并返回這個 promise 對象。
此外,Promise 的很多處理內(nèi)部也是使用了 Promise.resolve 算法將值轉(zhuǎn)換為 promise 對象后再進行處理的。
Promise.reject(error)是和 Promise.resolve(value) 類似的靜態(tài)方法,是 new Promise() 方法的快捷方式。
比如 Promise.reject(new Error("出錯了")) 就是下面代碼的語法糖形式。
new Promise(function(resolve,reject){
reject(new Error("出錯了"));
});
這段代碼的功能是調(diào)用該 promise 對象通過 then 指定的 onRejected 函數(shù),并將錯誤(Error)對象傳遞給這個 onRejected 函數(shù)。
Promise.reject(new Error("BOOM!")).catch(function(error){
console.error(error);
});
它和 Promise.resolve(value) 的不同之處在于 promise 內(nèi)調(diào)用的函數(shù)是 reject 而不是 resolve,這在編寫測試代碼或者進行 debug 時,說不定會用得上。
在使用 Promise.resolve(value) 等方法的時候,如果 promise 對象立刻就能進入 resolve 狀態(tài)的話,那么你是不是覺得 .then 里面指定的方法就是同步調(diào)用的呢?
實際上,.then 中指定的方法調(diào)用是異步進行的。
var promise = new Promise(function (resolve){
console.log("inner promise"); // 1
resolve(42);
});
promise.then(function(value){
console.log(value); // 3
});
console.log("outer promise"); // 2
執(zhí)行上面的代碼會輸出下面的 log,從這些 log 我們清楚地知道了上面代碼的執(zhí)行順序。
inner promise // 1
outer promise // 2
42// 3
由于 JavaScript 代碼會按照文件的從上到下的順序執(zhí)行,所以最開始 <1> 會執(zhí)行,然后是 resolve(42); 被執(zhí)行。這時候 promise 對象的已經(jīng)變?yōu)榇_定狀態(tài),F(xiàn)ulFilled 被設置為了 42 。
下面的代碼 promise.then 注冊了 <3> 這個回調(diào)函數(shù),這是本專欄的焦點問題。
由于 promise.then 執(zhí)行的時候 promise 對象已經(jīng)是確定狀態(tài),從程序上說對回調(diào)函數(shù)進行同步調(diào)用也是行得通的。
但是即使在調(diào)用 promise.then 注冊回調(diào)函數(shù)的時候 promise 對象已經(jīng)是確定的狀態(tài),Promise 也會以異步的方式調(diào)用該回調(diào)函數(shù),這是在 Promise 設計上的規(guī)定方針。
因此 <2> 會最先被調(diào)用,最后才會調(diào)用回調(diào)函數(shù) <3> 。
為什么要對明明可以以同步方式進行調(diào)用的函數(shù),非要使用異步的調(diào)用方式呢?
其實在 Promise 之外也存在這個問題,這里我們以一般的使用情況來考慮此問題。
這個問題的本質(zhì)是接收回調(diào)函數(shù)的函數(shù),會根據(jù)具體的執(zhí)行情況,可以選擇是以同步還是異步的方式對回調(diào)函數(shù)進行調(diào)用。
下面我們以 onReady(fn) 為例進行說明,這個函數(shù)會接收一個回調(diào)函數(shù)進行處理。
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
fn();
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
mixed-onready.js 會根據(jù)執(zhí)行時 DOM 是否已經(jīng)裝載完畢來決定是對回調(diào)函數(shù)進行同步調(diào)用還是異步調(diào)用。
如果在調(diào)用 onReady 之前 DOM 已經(jīng)載入的話::
對回調(diào)函數(shù)進行同步調(diào)用
如果在調(diào)用onReady之前DOM還沒有載入的話::
通過注冊 DOMContentLoaded 事件監(jiān)聽器來對回調(diào)函數(shù)進行異步調(diào)用
因此,如果這段代碼在源文件中出現(xiàn)的位置不同,在控制臺上打印的 log 消息順序也會不同。
為了解決這個問題,我們可以選擇統(tǒng)一使用異步調(diào)用的方式。
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
setTimeout(fn, 0);
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
關于這個問題,在 Effective JavaScript 的 第67 項 不要對異步回調(diào)函數(shù)進行同步調(diào)用 中也有詳細介紹。
setTimeout 等異步 API。Effective JavaScript
— David Herman
前面我們看到的 promise.then 也屬于此類,為了避免上述中同時使用同步、異步調(diào)用可能引起的混亂問題,Promise 在規(guī)范上規(guī)定 Promise只能使用異步調(diào)用方式 。
最后,如果將上面的 onReady 函數(shù)用 Promise 重寫的話,代碼如下面所示。
function onReadyPromise() {
return new Promise(function (resolve, reject) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', resolve);
}
});
}
onReadyPromise().then(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
由于 Promise 保證了每次調(diào)用都是以異步方式進行的,所以我們在實際編碼中不需要調(diào)用 setTimeout 來自己實現(xiàn)異步調(diào)用。
在前面的章節(jié)里我們對 Promise 基本的實例方法 then 和 catch 的使用方法進行了說明。
這其中,我想大家已經(jīng)認識了 .then().catch() 這種鏈式方法的寫法了,其實在 Promise 里可以將任意個方法連在一起作為一個方法鏈(method chain)。
promise 可以寫成方法鏈的形式
aPromise.then(function taskA(value){
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
console.log(error);
});
如果把在 then 中注冊的每個回調(diào)函數(shù)稱為 task 的話,那么我們就可以通過 Promise 方法鏈方式來編寫能以 taskA -> task B 這種流程進行處理的邏輯了。
Promise 方法鏈這種叫法有點長(其實是在日語里有點長,中文還可以 --譯者注),因此后面我們會簡化為 promise chain 這種叫法。
Promise 之所以適合編寫異步處理較多的應用,promise chain 可以算得上是其中的一個原因吧。
在本小節(jié),我們將主要針對使用 then 的 promise chain 的行為和流程進行學習。
在第一章 promise chain 里我們看到了一個很簡單的 then → catch 的例子,如果我們將方法鏈的長度變得更長的話,那在每個 promise 對象中注冊的 onFulfilled 和 onRejected 將會怎樣執(zhí)行呢?
promise chain - 即方法鏈越短越好。 在這個例子里我們是為了方便說明才選擇了較長的方法鏈。
我們先來看看下面這樣的 promise chain。
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
上面代碼中的 promise chain 的執(zhí)行流程,如果用一張圖來描述一下的話,像下面的圖那樣。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/2.4.1.png" alt="picture2.4.1" />
Figure 3. promise-then-catch-flow.js 附圖
在 上述代碼 中,我們沒有為 then 方法指定第二個參數(shù)(onRejected),也可以像下面這樣來理解。
then::
注冊 onFulfilled 時的回調(diào)函數(shù)
catch::
注冊 onRejected 時的回調(diào)函數(shù)
再看一下 上面的流程圖 的話,我們會發(fā)現(xiàn) Task A 和 Task B 都有指向 onRejected 的線出來。
這些線的意思是在 Task A 或 Task B 的處理中,在下面的情況下就會調(diào)用 onRejected 方法。
在 第一章 中我們已經(jīng)看到,Promise 中的處理習慣上都會采用 try-catch 的風格,當發(fā)生異常的時候,會被 catch 捕獲并被由在此函數(shù)注冊的回調(diào)函數(shù)進行錯誤處理。
另一種異常處理策略是通過 返回一個 Rejected 狀態(tài)的 promise 對象 來實現(xiàn)的,這種方法不通過使用 throw 就能在 promise chain 中對 onRejected 進行調(diào)用。
關于這種方法由于和本小節(jié)關系不大就不在這里詳述了,大家可以參考一下第4章 使用reject而不是throw 中的內(nèi)容。
此外在 promise chain中,由于在 onRejected 和 Final Task 后面沒有 catch 處理了,因此在這兩個 Task 中如果出現(xiàn)異常的話將不會被捕獲,這點需要注意一下。
下面我們再來看一個具體的關于 Task A -> onRejected 的例子。
Task A 產(chǎn)生異常的例子
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/4.png" alt="picture4" />
Figure 4. Task A 產(chǎn)生異常時的示意圖
將上面流程寫成代碼的話如下所示。
function taskA() {
console.log("Task A");
throw new Error("throw Error @ Task A")
}
function taskB() {
console.log("Task B");// 不會被調(diào)用
}
function onRejected(error) {
console.log(error);// => "throw Error @ Task A"
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
執(zhí)行這段代碼我們會發(fā)現(xiàn) Task B 是不會被調(diào)用的。
在本例中我們在 taskA 中使用了
throw方法故意制造了一個異常。但在實際中想主動進行 onRejected 調(diào)用的時候,應該返回一個 Rejected 狀態(tài)的 promise 對象。關于這種兩種方法的異同,請參考 使用 reject 而不是 throw 中的講解。
前面例子中的 Task 都是相互獨立的,只是被簡單調(diào)用而已。
這時候如果 Task A 想給 Task B 傳遞一個參數(shù)該怎么辦呢?
答案非常簡單,那就是在 Task A 中 return 的返回值,會在 Task B 執(zhí)行時傳給它。
我們還是先來看一個具體例子吧。
function doubleUp(value) {
return value * 2;
}
function increment(value) {
return value + 1;
}
function output(value) {
console.log(value);// => (1 + 1) * 2
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error){
// promise chain中出現(xiàn)異常的時候會被調(diào)用
console.error(error);
});
這段代碼的入口函數(shù)是 Promise.resolve(1); ,整體的 promise chain 執(zhí)行流程如下所示。
Promise.resolve(1); 傳遞 1 給 increment 函數(shù)increment 對接收的參數(shù)進行 +1 操作并返回(通過 return)doubleUp 函數(shù)output 中打印結(jié)果http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/5.png" alt="picture5" />
Figure 5. promise-then-passing-value.js 示意圖
每個方法中 return 的值不僅只局限于字符串或者數(shù)值類型,也可以是對象或者 promise 對象等復雜類型。
return 的值會由 Promise.resolve(return 的返回值); 進行相應的包裝處理,因此不管回調(diào)函數(shù)中會返回一個什么樣的值,最終 then 的結(jié)果都是返回一個新創(chuàng)建的 promise 對象。
關于這部分內(nèi)容可以參考 專欄: 每次調(diào)用then都會返回一個新創(chuàng)建的promise對象 ,那里也對一些常見錯誤進行了介紹。
也就是說, Promise#then 不僅僅是注冊一個回調(diào)函數(shù)那么簡單,它還會將回調(diào)函數(shù)的返回值進行變換,創(chuàng)建并返回一個 promise 對象。
在前面的 Promise#then 的章節(jié)里,我們已經(jīng)簡單地使用了 Promise#catch 方法。
這里我們再說一遍,實際上 Promise#catch 只是 promise.then(undefined, onRejected); 方法的一個別名而已。 也就是說,這個方法用來注冊當 promise 對象狀態(tài)變?yōu)?Rejected 時的回調(diào)函數(shù)。
關于如何根據(jù)場景使用 Promise#then 和 Promise#catch 可以參考 then or catch? 中介紹的內(nèi)容。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/5.1.png" alt="picture5.1" />
上面的這張圖,是下面這段代碼在使用 polyfill 的情況下在個瀏覽器上執(zhí)行的結(jié)果。
polyfill 是一個支持在不具備某一功能的瀏覽器上使用該功能的 Library。 這里我們使用的例子則來源于 jakearchibald/es6-promise 。
Promise#catch 的運行結(jié)果
var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
console.error(error);
});
如果我們在各種瀏覽器中執(zhí)行這段代碼,那么在 IE8 及以下版本則會出現(xiàn) identifier not found 的語法錯誤。
這是怎么回事呢? 實際上這和 catch 是 ECMAScript 的 保留字 (Reserved Word)有關。
在 ECMAScript 3 中保留字是不能作為對象的屬性名使用的。 而 IE8 及以下版本都是基于 ECMAScript 3 實現(xiàn)的,因此不能將 catch 作為屬性來使用,也就不能編寫類似 promise.catch() 的代碼,因此就出現(xiàn)了 identifier not found 這種語法錯誤了。
而現(xiàn)在的瀏覽器都是基于 ECMAScript 5 的,而在 ECMAScript 5中保留字都屬于 IdentifierName ,也可以作為屬性名使用了。
在 ECMAScript5 中保留字也不能作為 Identifiehttp://es5.github.io/#x7.6r 即變量名或方法名使用。 如果我們定義了一個名為
for的變量的話,那么就不能和循環(huán)語句的for區(qū)分了。 而作為屬性名的話,我們還是很容易區(qū)分object.for和for當然,我們也可以想辦法回避這個 ECMAScript 3保留字帶來的問題。
點標記法(dot notation) 要求對象的屬性必須是有效的標識符(在 ECMAScript 3中則不能使用保留字),
但是使用 中括號標記法(bracket notation) 的話,則可以將非合法標識符作為對象的屬性名使用。
也就是說,上面的代碼如果像下面這樣重寫的話,就能在 IE8 及以下版本的瀏覽器中運行了(當然還需要 polyfill)。 的,仔細想想我們就應該能接受將保留字作為屬性名來使用了。
解決 Promise#catch 標識符沖突問題
var promise = Promise.reject(new Error("message"));
promise["catch"](function (error) {
console.error(error);
});
或者我們不單純的使用 catch ,而是使用 then 也是可以避免這個問題的。
使用 Promise#then 代替 Promise#catch
var promise = Promise.reject(new Error("message"));
promise.then(undefined, function (error) {
console.error(error);
});
由于 catch 標識符可能會導致問題出現(xiàn),因此一些類庫(Library)也采用了 caught 作為函數(shù)名,而函數(shù)要完成的工作是一樣的。
而且很多壓縮工具自帶了將 promise.catch 轉(zhuǎn)換為 promise["catch"] 的功能, 所以可能不經(jīng)意之間也能幫我們解決這個問題。
如果各位讀者需要支持 IE8 及以下版本的瀏覽器的話,那么一定要將這個 catch 問題牢記在心中。
從代碼上乍一看, aPromise.then(...).catch(...) 像是針對最初的 aPromise 對象進行了一連串的方法鏈調(diào)用。
然而實際上不管是 then 還是 catch 方法調(diào)用,都返回了一個新的 promise 對象。
下面我們就來看看如何確認這兩個方法返回的到底是不是新的 promise 對象。
var aPromise = new Promise(function (resolve) {
resolve(100);
});
var thenPromise = aPromise.then(function (value) {
console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true
=== 是嚴格相等比較運算符,我們可以看出這三個對象都是互不相同的,這也就證明了 then 和 catch 都返回了和調(diào)用者不同的 promise 對象。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/2.6.1.png" alt="picture2.6.1" />
我們在對 Promise 進行擴展的時候需要牢牢記住這一點,否則稍不留神就有可能對錯誤的 promise 對象進行了處理。
如果我們知道了 then 方法每次都會創(chuàng)建并返回一個新的 promise 對象的話,那么我們就應該不難理解下面代碼中對 then 的使用方式上的差別了。
// 1: 對同一個promise對象同時調(diào)用 `then` 方法
var aPromise = new Promise(function (resolve) {
resolve(100);
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
console.log("1: " + value); // => 100
})
// vs
// 2: 對 `then` 進行 promise chain 方式進行調(diào)用
var bPromise = new Promise(function (resolve) {
resolve(100);
});
bPromise.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log("2: " + value); // => 100 * 2 * 2
});
第 1 種寫法中并沒有使用 promise 的方法鏈方式,這在 Promise 中是應該極力避免的寫法。這種寫法中的 then 調(diào)用幾乎是在同時開始執(zhí)行的,而且傳給每個 then 方法的 value 值都是 100 。
第 2 中寫法則采用了方法鏈的方式將多個 then 方法調(diào)用串連在了一起,各函數(shù)也會嚴格按照 resolve → then → then → then 的順序執(zhí)行,并且傳給每個 then 方法的 value 的值都是前一個 promise 對象通過 return 返回的值。
下面是一個由方法 1 中的 then 用法導致的比較容易出現(xiàn)的很有代表性的反模式的例子。
? then 的錯誤使用方法
function badAsyncCall() {
var promise = Promise.resolve();
promise.then(function() {
// 任意處理
return newVar;
});
return promise;
}
這種寫法有很多問題,首先在 promise.then 中產(chǎn)生的異常不會被外部捕獲,此外,也不能得到 then 的返回值,即使其有返回值。
由于每次 promise.then 調(diào)用都會返回一個新創(chuàng)建的 promise 對象,因此需要像上述方式 2 那樣,采用 promise chain 的方式將調(diào)用進行鏈式化,修改后的代碼如下所示。
then 返回返回新創(chuàng)建的 promise 對象
function anAsyncCall() {
var promise = Promise.resolve();
return promise.then(function() {
// 任意處理
return newVar;
});
}
關于這些反模式,詳細內(nèi)容可以參考 Promise Anti-patterns 。
這種函數(shù)的行為貫穿在 Promise 整體之中, 包括我們后面要進行說明的 Promise.all 和 Promise.race ,他們都會接收一個 promise 對象為參數(shù),并返回一個和接收參數(shù)不同的、新的 promise 對象。
到目前為止我們已經(jīng)學習了如何通過 .then 和 .catch 來注冊回調(diào)函數(shù),這些回調(diào)函數(shù)會在 promise 對象變?yōu)?FulFilled 或 Rejected 狀態(tài)之后被調(diào)用。
如果只有一個 promise 對象的話我們可以像前面介紹的那樣編寫代碼就可以了,如果要在多個 promise 對象都變?yōu)?FulFilled 狀態(tài)的時候才要進行某種處理話該如何操作呢?
我們以當所有 XHR(異步處理)全部結(jié)束后要進行某操作為例來進行說明。
各位讀者現(xiàn)在也許有點難以在大腦中描繪出這么一種場景,我們可以先看一下下面使用了普通的回調(diào)函數(shù)風格的 XHR 處理代碼。
function getURLCallback(URL, callback) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
callback(null, req.responseText);
} else {
callback(new Error(req.statusText), req.response);
}
};
req.onerror = function () {
callback(new Error(req.statusText));
};
req.send();
}
// <1> 對JSON數(shù)據(jù)進行安全的解析
function jsonParse(callback, error, value) {
if (error) {
callback(error, value);
} else {
try {
var result = JSON.parse(value);
callback(null, result);
} catch (e) {
callback(e, value);
}
}
}
// <2> 發(fā)送XHR請求
var request = {
comment: function getComment(callback) {
return getURLCallback('http://azu.github.io/promises-book/json/comment.json', jsonParse.bind(null, callback));
},
people: function getPeople(callback) {
return getURLCallback('http://azu.github.io/promises-book/json/people.json', jsonParse.bind(null, callback));
}
};
// <3> 啟動多個XHR請求,當所有請求返回時調(diào)用callback
function allRequest(requests, callback, results) {
if (requests.length === 0) {
return callback(null, results);
}
var req = requests.shift();
req(function (error, value) {
if (error) {
callback(error, value);
} else {
results.push(value);
allRequest(requests, callback, results);
}
});
}
function main(callback) {
allRequest([request.comment, request.people], callback, []);
}
// 運行的例子
main(function(error, results){
if(error){
return console.error(error);
}
console.log(results);
});
這段回調(diào)函數(shù)風格的代碼有以下幾個要點。
JSON.parse 函數(shù)的話可能會拋出異常,所以這里使用了一個包裝函數(shù) jsonParseallRequest 函數(shù)并在其中對 request 進行調(diào)用。callback(error,value) 這種寫法,第一個參數(shù)表示錯誤信息,第二個參數(shù)為返回值在使用 jsonParse 函數(shù)的時候我們使用了 bind 進行綁定,通過使用這種偏函數(shù)(Partial Function)的方式就可以減少匿名函數(shù)的使用。(如果在函數(shù)回調(diào)風格的代碼能很好的做到函數(shù)分離的話,也能減少匿名函數(shù)的數(shù)量)
jsonParse.bind(null, callback);
// 可以認為這種寫法能轉(zhuǎn)換為以下的寫法
function bindJSONParse(error, value){
jsonParse(callback, error, value);
}
在這段回調(diào)風格的代碼中,我們也能發(fā)現(xiàn)如下一些問題。
下面我們再來看看如何使用 Promise#then 來完成同樣的工作
需要事先說明的是 Promise.all 比較適合這種應用場景的需求,因此我們故意采用了大量 .then 的晦澀的寫法。
使用了 .then 的話,也并不是說能和回調(diào)風格完全一致,大概重寫后代碼如下所示。
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
function recordValue(results, value) {
results.push(value);
return results;
}
// [] 用來保存初始化的值
var pushValue = recordValue.bind(null, []);
return request.comment().then(pushValue).then(request.people).then(pushValue);
}
// 運行的例子
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.error(error);
});
將上述代碼和回調(diào)函數(shù)風格相比,我們可以得到如下結(jié)論。
JSON.parse 函數(shù)main() 返回 promise 對象向前面我們說的那樣,main 的 then 部分有點晦澀難懂。
為了應對這種需要對多個異步調(diào)用進行統(tǒng)一處理的場景,Promise 準備了 Promise.all 和 Promise.race 這兩個靜態(tài)方法。
在下面的小節(jié)中我們將對這兩個函數(shù)進行說明。
Promise.all 接收一個 promise 對象的數(shù)組作為參數(shù),當這個數(shù)組里的所有 promise 對象全部變?yōu)?resolve 或 rejec t狀態(tài)的時候,它才會去調(diào)用 .then 方法。
前面我們看到的批量獲得若干 XHR 的請求結(jié)果的例子,使用 Promise.all 的話代碼會非常簡單。
之前例子中的 getURL 返回了一個 promise 對象,它封裝了 XHR 通信的實現(xiàn)。 向 Promise.all 傳遞一個由封裝了 XHR 通信的 promise 對象數(shù)組的話,則只有在全部的 XHR 通信完成之后(變?yōu)?FulFilled 或 Rejected 狀態(tài))之后,才會調(diào)用 .then 方法。
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
return Promise.all([request.comment(), request.people()]);
}
// 運行示例
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.log(error);
});
這個例子的執(zhí)行方法和 前面的例子 一樣。 不過 Promise.all 在以下幾點和之前的例子有所不同。
Promise.all([request.comment(), request.people()]);
在上面的代碼中,request.comment() 和 request.people() 會同時開始執(zhí)行,而且每個 promise 的結(jié)果(resolve 或 reject 時傳遞的參數(shù)值),和傳遞給 Promise.all 的 promise 數(shù)組的順序是一致的。
也就是說,這時候 .then 得到的 promise 數(shù)組的執(zhí)行結(jié)果的順序是固定的,即 [comment, people]。
main().then(function (results) {
console.log(results); // 按照[comment, people]的順序
});
如果像下面那樣使用一個計時器來計算一下程序執(zhí)行時間的話,那么就可以非常清楚的知道傳遞給 Promise.all 的 promise 數(shù)組是同時開始執(zhí)行的。
// `delay`毫秒后執(zhí)行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
var startDate = Date.now();
// 所有promise變?yōu)閞esolve后程序退出
Promise.all([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (values) {
console.log(Date.now() - startDate + 'ms');
// 約128ms
console.log(values);// [1,32,64,128]
});
timerPromisefy 會每隔一定時間(通過參數(shù)指定)之后,返回一個 promise 對象,狀態(tài)為 FulFilled,其狀態(tài)值為傳給 timerPromisefy 的參數(shù)。
而傳給 Promise.all 的則是由上述 promise 組成的數(shù)組。
var promises = [
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
這時候,每隔1, 32, 64, 128 ms都會有一個 promise 發(fā)生 resolve 行為。
也就是說,這個 promise 對象數(shù)組中所有 promise 都變?yōu)?resolve 狀態(tài)的話,至少需要 128 ms。實際我們計算一下 Promise.all 的執(zhí)行時間的話,它確實是消耗了 128 ms 的時間。
從上述結(jié)果可以看出,傳遞給 Promise.all 的 promise 并不是一個個的順序執(zhí)行的,而是同時開始、并行執(zhí)行的。
如果這些 promise 全部串行處理的話,那么需要 等待 1 ms → 等待 32 ms → 等待 64 ms → 等待 128 ms ,全部執(zhí)行完畢需要 225 ms 的時間。
要想了解更多關于如何使用 Promise 進行串行處理的內(nèi)容,可以參考第 4 章的 Promise 中的串行處理中的介紹。
接著我們來看看和 Promise.all 類似的對多個 promise 對象進行處理的 Promise.race 方法。
它的使用方法和 Promise.all 一樣,接收一個 promise 對象數(shù)組為參數(shù)。
Promise.all 在接收到的所有的對象 promise 都變?yōu)?FulFilled 或者 Rejected 狀態(tài)之后才會繼續(xù)進行后面的處理, 與之相對的是 Promise.race 只要有一個 promise 對象進入 FulFilled 或者 Rejected 狀態(tài)的話,就會繼續(xù)進行后面的處理。
像 Promise.all 時的例子一樣,我們來看一個帶計時器的 Promise.race 的使用例子。
// `delay`毫秒后執(zhí)行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
// 任何一個promise變?yōu)閞esolve或reject 的話程序就停止運行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (value) {
console.log(value); // => 1
});
上面的代碼創(chuàng)建了 4 個 promise 對象,這些 promise 對象會分別在 1 ms,32 ms,64 ms 和 128 ms 后變?yōu)榇_定狀態(tài),即 FulFilled,并且在第一個變?yōu)榇_定狀態(tài)的 1 ms 后, .then 注冊的回調(diào)函數(shù)就會被調(diào)用,這時候確定狀態(tài)的 promise 對象會調(diào)用 resolve(1) 因此傳遞給 value 的值也是 1,控制臺上會打印出 1 來。
下面我們再來看看在第一個 promise 對象變?yōu)榇_定(FulFilled)狀態(tài)后,它之后的 promise 對象是否還在繼續(xù)運行。
var winnerPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is winner');
resolve('this is winn