在這一章里,我們會基于前面學(xué)到的內(nèi)容,再深入了解一下 Promise 里的一些高級內(nèi)容,加深對 Promise 的理解。
在本小節(jié)里,我們將不打算對瀏覽器實現(xiàn)的 Promise 進行說明,而是要介紹一些第三方實現(xiàn)的和 Promise 兼容的類庫。
為什么需要這些類庫呢?我想有些讀者不免會有此疑問。首先能想到的原因是有些運行環(huán)境并不支持 ES6 Promises 。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/4.1.png" alt="picture4.1" />
當(dāng)我們在網(wǎng)上查找 Promise 的實現(xiàn)類庫的時候,有一個因素是首先要考慮的,那就是是否具有 Promises/A+兼容性 。
Promises/A+ 是 ES6 Promises 的前身,Promise 的 then 也是來自于此的基于社區(qū)的規(guī)范。
如果說一個類庫兼容 Promises/A+ 的話,那么就是說它除了具有標(biāo)準(zhǔn)的 then 方法之外,很多情況下也說明此類庫還支持 Promise.all 和 catch 等功能。
但是 Promises/A+ 實際上只是定義了關(guān)于 Promise#then 的規(guī)范,所以有些類庫可能實現(xiàn)了其它諸如 all 或 catch 等功能,但是可能名字卻不一樣。
如果我們說一個類庫具有 then 兼容性的話,實際上指的是 Thenable ,它通過使用 Promise.resolve 基于 ES6 Promise 的規(guī)定,進行 promise 對象的變換。
ES6 Promise 里關(guān)于 promise 對象的規(guī)定包括在使用
catch方法,或使用Promise.all進行處理的時候不能出現(xiàn)錯誤。
在這些 Promise 的實現(xiàn)類庫中,我們這里主要對兩種類型的類庫進行介紹。
一種是被稱為 Polyfill (這是一款英國產(chǎn)品,就是裝修刮墻用的膩子,其意義可想而知?—?譯者注)的類庫,另一種是即具有 Promises/A+兼容性 ,又增加了自己獨特功能的類庫。
Promise 的實現(xiàn)類庫數(shù)量非常之多,這里我們只是介紹了其中有限的幾個。
Polyfill
只需要在瀏覽器中加載 Polyfill 類庫,就能使用 IE10 等或者還沒有提供對 Promise 支持的瀏覽器中使用 Promise 里規(guī)定的方法。
也就是說如果加載了 Polyfill 類庫,就能在還不支持 Promise 的環(huán)境中,運行本文中的各種示例代碼。
jakearchibald/es6-promise
一個兼容 ES6 Promises 的 Polyfill 類庫。 它基于 RSVP.js 這個兼容 Promises/A+ 的類庫, 它只是 RSVP.js 的一個子集,只實現(xiàn)了 Promises 規(guī)定的 API。
yahoo/ypromise
這是一個獨立版本的 YUI 的 Promise Polyfill,具有和 ES6 Promises 的兼容性。 本書的示例代碼也都是基于這個 ypromise 的 Polyfill 來在線運行的。
getify/native-promise-only
以作為 ES6 Promises 的 polyfill 為目的的類庫 它嚴格按照 ES6 Promises 的規(guī)范設(shè)計,沒有添加在規(guī)范中沒有定義的功能。 如果運行環(huán)境有原生的 Promise 支持的話,則優(yōu)先使用原生的 Promise 支持。
Promise 擴展類庫
Promise 擴展類庫除了實現(xiàn)了 Promise 中定義的規(guī)范之外,還增加了自己獨自定義的功能。
Promise 擴展類庫數(shù)量非常的多,我們只介紹其中兩個比較有名的類庫。
kriskowal/q
類庫 Q 實現(xiàn)了 Promises 和 Deferreds 等規(guī)范。 它自 2009 年開始開發(fā),還提供了面向 Node.js 的文件 IO API Q-IO 等, 是一個在很多場景下都能用得到的類庫。
petkaantonov/bluebird 這個類庫除了兼容 Promise 規(guī)范之外,還擴展了取消 promise 對象的運行,取得 promise 的運行進度,以及錯誤處理的擴展檢測等非常豐富的功能,此外它在實現(xiàn)上還在性能問題下了很大的功夫。
Q 和 Bluebird 這兩個類庫除了都能在瀏覽器里運行之外,充實的 API reference 也是其特征。
Q 等文檔里詳細介紹了 Q 的 Deferred 和 jQuery 里的 Deferred 有哪些異同,以及要怎么進行遷移 Coming from jQuery 等都進行了詳細的說明。
Bluebird 的文檔除了提供了使用 Promise 豐富的實現(xiàn)方式之外,還涉及到了在出現(xiàn)錯誤時的對應(yīng)方法以及 Promise 中的反模式 等內(nèi)容。
這兩個類庫的文檔寫得都很友好,即使我們不使用這兩個類庫,閱讀一下它們的文檔也具有一定的參考價值。
本小節(jié)介紹了 Promise 的實現(xiàn)類庫中的 Polyfill 和擴展類庫這兩種。
Promise 的實現(xiàn)類庫種類繁多,到底選擇哪個來使用完全看自己的喜好了。
但是由于這些類庫實現(xiàn)的 Promise 同時具有 Promises/A+ 或 ES6 Promises 共通的接口,所以在使用某一類庫的時候,有時候也可以參考一下其他類庫的代碼或者擴展功能。
熟練掌握 Promise 中的共通概念,進而能在實際中能對這些技術(shù)運用自如,這也是本書的寫作目的之一。
在 第二章的 Promise.resolve 中我們已經(jīng)說過, Promise.resolve 的最大特征之一就是可以將 thenable 的對象轉(zhuǎn)換為 promise 對象。
在本小節(jié)里,我們將學(xué)習(xí)一下利用將 thenable 對象轉(zhuǎn)換為 promise 對象這個功能都能具體做些什么事情。
這里我們以桌面通知 API Web Notifications 為例進行說明。
關(guān)于 Web Notifications API 的詳細信息可以參考下面的網(wǎng)址。
簡單來說,Web Notifications API 就是能像以下代碼那樣通過 new Notification 來顯示通知消息。
new Notification("Hi!");
當(dāng)然,為了顯示通知消息,我們需要在運行 new Notification 之前,先獲得用戶的許可。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/11.png" alt="picture11" />
Figure 11. 確認是否允許 Notification 的對話框
用戶在這個是否允許 Notification 的對話框選擇后的結(jié)果,會通過 Notification.permission 傳給我們的程序,它的值可能是允許("granted")或拒絕("denied")這二者之一。
否允許 Notification 對話框中的可選項,在 Firefox 中除了允許、拒絕之外,還增加了 永久有效 和 會話范圍內(nèi)有效 兩種額外選項,當(dāng)然
Notification.permission的值都是一樣的。
在程序中可以通過 Notification.requestPermission() 來彈出是否允許 Notification 對話框, 用戶選擇的結(jié)果會通過 status 參數(shù)傳給回調(diào)函數(shù)。
從這個回調(diào)函數(shù)我們也可以看出來,用戶選擇允許還是拒絕通知是異步進行的。
Notification.requestPermission(function (status) {
// status的值為 "granted" 或 "denied"
console.log(status);
});
到用戶收到并顯示通知為止,整體的處理流程如下所示。
new Notification 顯示通知消息。這又分兩種情況
雖然上面說到了幾種情景,但是最終結(jié)果就是用戶允許或者拒絕,可以總結(jié)為如下兩種模式。
允許時("granted")
使用 new Notification 創(chuàng)建通知消息
拒絕時("denied") 沒有任何操作
這兩種模式是不是覺得有在哪里看過的感覺? 呵呵,用戶的選擇結(jié)果,正和在 Promise 中 promise 對象變?yōu)?Fulfilled 或 Rejected 狀態(tài)非常類似。
resolve(成功)時 == 用戶允許("granted")
調(diào)用 onFulfilled 方法
reject(失敗)時 == 用戶拒絕("denied")
調(diào)用 onRejected 函數(shù)
是不是我們可以用 Promise 的方式去編寫桌面通知的代碼呢?我們先從回調(diào)函數(shù)風(fēng)格的代碼入手看看到底怎么去做。
首先,我們以回到函數(shù)風(fēng)格的代碼對上面的 Web Notification API 包裝函數(shù)進行重寫,新代碼如下所示。
function notifyMessage(message, options, callback) {
if (Notification && Notification.permission === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
// 運行實例
// 第二個參數(shù)是傳給 `Notification` 的option對象
notifyMessage("Hi!", {}, function (error, notification) {
if(error){
return console.error(error);
}
console.log(notification);// 通知對象
});
在回調(diào)風(fēng)格的代碼里,當(dāng)用戶拒絕接收通知的時候, error 會被設(shè)置值,而如果用戶同意接收通知的時候,則會顯示通知消息并且 notification 會被設(shè)置值。
回調(diào)函數(shù)接收 error 和 notification 兩個參數(shù)
function callback(error, notification){
}
下面,我想再將這個回調(diào)函數(shù)風(fēng)格的代碼使用 Promise 進行改寫。
基于上述回調(diào)風(fēng)格的 notifyMessage 函數(shù),我們再來創(chuàng)建一個返回 promise 對象的 notifyMessageAsPromise 方法。
function notifyMessage(message, options, callback) {
if (Notification && Notification.permission === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
function notifyMessageAsPromise(message, options) {
return new Promise(function (resolve, reject) {
notifyMessage(message, options, function (error, notification) {
if (error) {
reject(error);
} else {
resolve(notification);
}
});
});
}
// 運行示例
notifyMessageAsPromise("Hi!").then(function (notification) {
console.log(notification);// 通知對象
}).catch(function(error){
console.error(error);
});
在用戶允許接收通知的時候,運行上面的代碼,會顯示 "Hi!" 消息。
當(dāng)用戶接收通知消息的時候, .then 函數(shù)會被調(diào)用,當(dāng)用戶拒絕接收消息的時候, .catch 方法會被調(diào)用。
由于瀏覽器是以網(wǎng)站為單位保存 Web Notifications API 的許可狀態(tài)的,所以實際上有下面四種模式存在。
已經(jīng)獲得用戶許可
.then方法被調(diào)用彈出詢問對話框并獲得許可
.then方法被調(diào)用已經(jīng)是被用戶拒絕的狀態(tài)
.catch方法被調(diào)用彈出詢問對話框并被用戶拒絕
.catch方法被調(diào)用也就是說,如果使用原生的 Web Notifications API 的話,那么需要在程序中對上述四種情況都進行處理,我們可以像下面的包裝函數(shù)那樣,將上述四種情況簡化為兩種以方便處理。
上面的 notification-as-promise.js 雖然看上去很方便,但是實際上使用的時候,很可能出現(xiàn) 在不支持 Promise 的環(huán)境下不能使用 的問題。
如果你想編寫像 notification-as-promise.js 這樣具有 Promise 風(fēng)格和的類庫的話,我覺得你有如下的一些選擇。
支持Promise的環(huán)境是前提
Promise Promise 的環(huán)境下不能正常工作(即應(yīng)該出錯)。在類庫中實現(xiàn)Promise
Promise 功能 在回調(diào)函數(shù)中也應(yīng)該能夠使用 Promise
notification-as-promise.js 就是以 Promise 存在為前提的寫法。
回歸正文,在這里 Thenable 是為了幫助實現(xiàn)在回調(diào)函數(shù)中也能使用 Promise 的一個概念。
我們已經(jīng)說過,thenable 就是一個具有.then 方法的一個對象。下面我們就在 notification-callback.js 中增加一個返回值為 thenable 類型的方法
function notifyMessage(message, options, callback) {
if (Notification && Notification.permission === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
// 返回 `thenable`
function notifyMessageAsThenable(message, options) {
return {
'then': function (resolve, reject) {
notifyMessage(message, options, function (error, notification) {
if (error) {
reject(error);
} else {
resolve(notification);
}
});
}
};
}
// 運行示例
Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
console.log(notification);// 通知對象
}).catch(function(error){
console.error(error);
});
notification-thenable.js 里增加了一個 notifyMessageAsThenable 方法。這個方法返回的對象具備一個 then 方法。
then 方法的參數(shù)和 new Promise(function (resolve, reject){}) 一樣,在確定時執(zhí)行 resolve 方法,拒絕時調(diào)用 reject 方法。
then 方法和 notification-as-promise.js 中的 notifyMessageAsPromise 方法完成了同樣的工作。
我們可以看出, Promise.resolve(thenable) 通過使用了 thenable 這個promise對象,就能利用 Promise 功能了。
Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
console.log(notification);// 通知對象
}).catch(function(error){
console.error(error);
});
使用了 Thenable 的 notification-thenable.js 和依賴于 Promise 的 notification-as-promise.js ,實際上都是非常相似的使用方法。
notification-thenable.js 和 notification-as-promise.js 比起來,有以下的不同點。
類庫側(cè)沒有提供 Promise 的實現(xiàn)
Promise.resolve(thenable) 來自己實現(xiàn)了 PromisePromise.resolve(thenable) 一起配合使用在本小節(jié)我們主要學(xué)習(xí)了什么是 Thenable,以及如何通過 Promise.resolve(thenable) 使用 Thenable,將其作為 promise 對象來使用。
Callback?—?Thenable?—?Promise
Thenable 風(fēng)格表現(xiàn)為位于回調(diào)和 Promise 風(fēng)格中間的一種狀態(tài),作為類庫的公開 API 有點不太成熟,所以并不常見。
Thenable 本身并不依賴于 Promise 功能,但是 Promise 之外也沒有使用 Thenable 的方式,所以可以認為 Thenable 間接依賴于 Promise。
另外,用戶需要對 Promise.resolve(thenable) 有所理解才能使用好 Thenable,因此作為類庫的公開 API 有一部分會比較難。和公開 API 相比,更多情況下是在內(nèi)部使用 Thenable。
在編寫異步處理的類庫的時候,推薦采用先編寫回調(diào)風(fēng)格的函數(shù),然后再轉(zhuǎn)換為公開 API 這種方式。
貌似 Node.js 的 Core module 就采用了這種方式,除了類庫提供的基本回調(diào)風(fēng)格的函數(shù)之外,用戶也可以通過 Promise 或者 Generator 等自己擅長的方式進行實現(xiàn)。
最初就是以能被 Promise 使用為目的的類庫,或者其本身依賴于 Promise 等情況下,我想將返回 promise 對象的函數(shù)作為公開 API 應(yīng)該也沒什么問題。
什么時候該使用 Thenable?
那么,又是在什么情況下應(yīng)該使用 Thenable 呢?
恐怕最可能被使用的是在 Promise 類庫 之間進行相互轉(zhuǎn)換了。
比如,類庫 Q 的 Promise 實例為 Q promise對象,提供了 ES6 Promises 的 promise 對象不具備的方法。Q promise 對象提供了 promise.finally(callback) 和 promise.nodeify(callback) 等方法。
如果你想將 ES6 Promises 的 promise 對象轉(zhuǎn)換為 Q promise 的對象,輪到 Thenable 大顯身手的時候就到了。
使用 thenable 將 promise 對象轉(zhuǎn)換為 Q promise 對象
var Q = require("Q");
// 這是一個ES6的promise對象
var promise = new Promise(function(resolve){
resolve(1);
});
// 變換為Q promise對象
Q(promise).then(function(value){
console.log(value);
}).finally(function(){ <1>
console.log("finally");
});
<1> 因為是 Q promise對象所以可以使用 finally 方法
上面代碼中最開始被創(chuàng)建的 promise 對象具備 then 方法,因此是一個 Thenable 對象。我們可以通過 Q(thenable)方法,將這個 Thenable 對象轉(zhuǎn)換為 Q promise 對象。
可以說它的機制和 Promise.resolve(thenable) 一樣,當(dāng)然反過來也一樣。
像這樣,Promise 類庫雖然都有自己類型的 promise 對象,但是它們之間可以通過 Thenable 這個共通概念,在類庫之間(當(dāng)然也包括 native Promise)進行 promise 對象的相互轉(zhuǎn)換。
我們看到,就像上面那樣,Thenable 多在類庫內(nèi)部實現(xiàn)中使用,所以從外部來說不會經(jīng)常看到 Thenable 的使用。但是我們必須牢記 Thenable 是 Promise 中一個非常重要的概念。
Promise 的構(gòu)造函數(shù),以及被 then 調(diào)用執(zhí)行的函數(shù)基本上都可以認為是在 try...catch 代碼塊中執(zhí)行的,所以在這些代碼中即使使用 throw ,程序本身也不會因為異常而終止。
如果在 Promise 中使用 throw 語句的話,會被 try...catch 住,最終 promise 對象也變?yōu)?Rejected 狀態(tài)。
var promise = new Promise(function(resolve, reject){
throw new Error("message");
});
promise.catch(function(error){
console.error(error);// => "message"
});
http://liubin.github.io/promises-book/#promise-states
代碼像這樣其實運行時倒也不會有什么問題,但是如果想把 promise 對象狀態(tài) 設(shè)置為 Rejected 狀態(tài)的話,使用 reject 方法則更顯得合理。
所以上面的代碼可以改寫為下面這樣。
var promise = new Promise(function(resolve, reject){
reject(new Error("message"));
});
promise.catch(function(error){
console.error(error);// => "message"
})
其實我們也可以這么來考慮,在出錯的時候我們并沒有調(diào)用 throw 方法,而是使用了 reject ,那么給 reject 方法傳遞一個 Error 類型的對象也就很好理解了。
話說回來,為什么在想將 promise 對象的狀態(tài)設(shè)置為 Rejected 的時候應(yīng)該使用 reject 而不是 throw 呢?
首先是因為我們很難區(qū)分 throw 是我們主動拋出來的,還是因為真正的其它 異常 導(dǎo)致的。
比如在使用 Chrome 瀏覽器的時候,Chrome 的開發(fā)者工具提供了在程序發(fā)生異常的時候自動在調(diào)試器中 break 的功能。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/12.png" alt="picture12" />
Figure 12. Pause On Caught Exceptions
當(dāng)我們開啟這個功能的時候,在執(zhí)行到下面代碼中的 throw 時就會觸發(fā)調(diào)試器的 break 行為。
var promise = new Promise(function(resolve, reject){
throw new Error("message");
});
本來這是和調(diào)試沒有關(guān)系的地方,也因為在 Promise 中的 throw 語句被 break 了,這也嚴重的影響了瀏覽器提供的此功能的正常使用。
在 Promise 構(gòu)造函數(shù)中,有一個用來指定 reject 方法的參數(shù),使用這個參數(shù)而不是依靠 throw 將 promise 對象的狀態(tài)設(shè)置為 Rejected 狀態(tài)非常簡單。
那么如果像下面那樣想在 then 中進行 reject 的話該怎么辦呢?
var promise = Promise.resolve();
promise.then(function (value) {
setTimeout(function () {
// 經(jīng)過一段時間后還沒處理完的話就進行reject - 2
}, 1000);
// 比較耗時的處理 - 1
somethingHardWork();
}).catch(function (error) {
// 超時錯誤 - 3
});
上面的超時處理,需要在 then 中進行 reject 方法調(diào)用,但是傳遞給當(dāng)前的回調(diào)函數(shù)的參數(shù)只有前面的一 promise 對象,這該怎么辦呢?
關(guān)于使用 Promise 進行超時處理的具體實現(xiàn)方法可以參考 使用 Promise.race 和 delay 取消 XHR 請求 中的詳細說明。
在這里我們再次回憶下 then 的工作原理。
在 then 中注冊的回調(diào)函數(shù)可以通過 return 返回一個值,這個返回值會傳給后面的 then 或 catch 中的回調(diào)函數(shù)。
而且 return 的返回值類型不光是簡單的字面值,還可以是復(fù)雜的對象類型,比如 promise 對象等。
這時候,如果返回的是 promise 對象的話,那么根據(jù)這個 promise 對象的狀態(tài),在下一個 then 中注冊的回調(diào)函數(shù)中的 onFulfilled 和 onRejected 的哪一個會被調(diào)用也是能確定的。
var promise = Promise.resolve();
promise.then(function () {
var retPromise = new Promise(function (resolve, reject) {
// resolve or reject 的狀態(tài)決定 onFulfilled or onRejected 的哪個方法會被調(diào)用
});
return retPromise;<1>
}).then(onFulfilled, onRejected);
<1> 后面的 then 調(diào)用哪個回調(diào)函數(shù)是由 promise 對象的狀態(tài)來決定的
也就是說,這個 retPromise 對象狀態(tài)為 Rejected 的時候,會調(diào)用后面 then 中的 onRejected 方法,這樣就實現(xiàn)了即使在 then 中不使用 throw 也能進行 reject 處理了。
ar onRejected = console.error.bind(console);
var promise = Promise.resolve();
promise.then(function () {
var retPromise = new Promise(function (resolve, reject) {
reject(new Error("this promise is rejected"));
});
return retPromise;
}).catch(onRejected);
使用 Promise.reject 的話還能再將代碼進行簡化。
var onRejected = console.error.bind(console);
var promise = Promise.resolve();
promise.then(function () {
return Promise.reject(new Error("this promise is rejected"));
}).catch(onRejected);
在本小節(jié)我們主要學(xué)習(xí)了
使用 reject 會比使用 throw 安全
then 中使用 reject 的方法也許實際中我們可能不常使用 reject ,但是比起來不假思索的使用 throw 來說,使用 reject 的好處還是很多的。
關(guān)于上面講的內(nèi)容的比較詳細的例子,大家可以參考在 使用 Promise.race 和 delay 取消 XHR 請求 小節(jié)的介紹。
這一節(jié)我們來簡單介紹下 Deferred 和 Promise 之間的關(guān)系
說起 Promise ,我想大家一定同時也聽說過 Deferred 這個術(shù)語。比如 jQuery.Deferred 和 JSDeferred 等,一定都是大家非常熟悉的內(nèi)容了。
Deferred 和 Promise不同,它沒有共通的規(guī)范,每個 Library 都是根據(jù)自己的喜好來實現(xiàn)的。
在這里,我們打算以 jQuery.Deferred 類似的實現(xiàn)為中心進行介紹。
簡單來說,Deferred 和 Promise 具有如下的關(guān)系。
Deferred 擁有 Promise
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/13.png" alt="picture13" />
Figure 13. Deferred和Promise
我想各位看到此圖應(yīng)該就很容易理解了,Deferred 和 Promise 并不是處于競爭的關(guān)系,而是 Deferred 內(nèi)涵了 Promise。
這是 jQuery.Deferred 結(jié)構(gòu)的簡化版。當(dāng)然也有的 Deferred 實現(xiàn)并沒有內(nèi)涵 Promise。
光看圖的話也許還難以理解,下面我們就看看看怎么通過 Promise 來實現(xiàn) Deferred。
基于 Promise 實現(xiàn) Deferred 的例子。
function Deferred() {
this.promise = new Promise(function (resolve, reject) {
this._resolve = resolve;
this._reject = reject;
}.bind(this));
}
Deferred.prototype.resolve = function (value) {
this._resolve.call(this.promise, value);
};
Deferred.prototype.reject = function (reason) {
this._reject.call(this.promise, reason);
};
我們再將之前使用 Promise 實現(xiàn)的 getURL 用 Deferred 改寫一下。
function Deferred() {
this.promise = new Promise(function (resolve, reject) {
this._resolve = resolve;
this._reject = reject;
}.bind(this));
}
Deferred.prototype.resolve = function (value) {
this._resolve.call(this.promise, value);
};
Deferred.prototype.reject = function (reason) {
this._reject.call(this.promise, reason);
};
function getURL(URL) {
var deferred = new Deferred();
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
deferred.resolve(req.responseText);
} else {
deferred.reject(new Error(req.statusText));
}
};
req.onerror = function () {
deferred.reject(new Error(req.statusText));
};
req.send();
return deferred.promise;
}
// 運行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(console.error.bind(console));
所謂的能對 Promise 狀態(tài)進行操作的特權(quán)方法,指的就是能對 promise 對象的狀態(tài)進行 resolve、reject 等調(diào)用的方法,而通常的 Promise 的話只能在通過構(gòu)造函數(shù)傳遞的方法之內(nèi)對 promise 對象的狀態(tài)進行操作。
我們來看看 Deferred 和 Promise 相比在實現(xiàn)上有什么異同。
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 URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(console.error.bind(console));
對比上述兩個版本的 getURL ,我們發(fā)現(xiàn)它們有如下不同。
Deferred 的話不需要將代碼用 Promise 括起來
在以下方面,它們則完成了同樣的工作。
resolve、reject 的時機由于 Deferred 包含了 Promise,所以大體的流程還是差不多的,不過 Deferred 有用對 Promise 進行操作的特權(quán)方法,以及高度自由的對流程控制進行自由定制。
比如在 Promise 一般都會在構(gòu)造函數(shù)中編寫主要處理邏輯,對 resolve、reject 方法的調(diào)用時機也基本是很確定的。
new Promise(function (resolve, reject){
// 在這里進行promise對象的狀態(tài)確定
});
而使用 Deferred 的話,并不需要將處理邏輯寫成一大塊代碼,只需要先創(chuàng)建 deferred 對象,可以在任何時機對 resolve、reject 方法進行調(diào)用。
var deferred = new Deferred();
// 可以在隨意的時機對 `resolve`、`reject` 方法進行調(diào)用
上面我們只是簡單的實現(xiàn)了一個 Deferred ,我想你已經(jīng)看到了它和 Promise 之間的差異了吧。
如果說 Promise 是用來對值進行抽象的話,Deferred 則是對處理還沒有結(jié)束的狀態(tài)或操作進行抽象化的對象,我們也可以從這一層的區(qū)別來理解一下這兩者之間的差異。
換句話說,Promise 代表了一個對象,這個對象的狀態(tài)現(xiàn)在還不確定,但是未來一個時間點它的狀態(tài)要么變?yōu)檎V担‵ulFilled),要么變?yōu)楫惓V担≧ejected);而 Deferred 對象表示了一個處理還沒有結(jié)束的這種事實,在它的處理結(jié)束的時候,可以通過 Promise 來取得處理結(jié)果。
如果各位讀者還想深入了解一下 Deferred 的話,可以參考下面的這些資料。
Deferred 最初是在 Python 的 Twisted 框架中被提出來的概念。 在 JavaScript 領(lǐng)域可以認為它是由 MochiKit.Async 、 dojo/Deferred 等 Library 引入的。
在本小節(jié)中,作為在第 2 章所學(xué)的 Promise.race 的具體例子,我們來看一下如何使用 Promise.race 來實現(xiàn)超時機制。
當(dāng)然 XHR 有一個 timeout 屬性,使用該屬性也可以簡單實現(xiàn)超時功能,但是為了能支持多個 XHR 同時超時或者其他功能,我們采用了容易理解的異步方式在 XHR 中通過超時來實現(xiàn)取消正在進行中的操作。
首先我們來看一下如何在 Promise 中實現(xiàn)超時。
所謂超時就是要在經(jīng)過一定時間后進行某些操作,使用 setTimeout 的話很好理解。
首先我們來串講一個單純的在 Promise 中調(diào)用 setTimeout 的函數(shù)。
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
delayPromise(ms) 返回一個在經(jīng)過了參數(shù)指定的毫秒數(shù)后進行 onFulfilled 操作的 promise 對象,這和直接使用 setTimeout 函數(shù)比較起來只是編碼上略有不同,如下所示。
setTimeout(function () {
alert("已經(jīng)過了100ms!");
}, 100);
// == 幾乎同樣的操作
delayPromise(100).then(function () {
alert("已經(jīng)過了100ms!");
});
在這里 promise 對象 這個概念非常重要,請切記。
讓我們回顧一下靜態(tài)方法 Promise.race ,它的作用是在任何一個 promise 對象進入到確定(解決)狀態(tài)后就繼續(xù)進行后續(xù)處理,如下面的例子所示。
var winnerPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is winner');
resolve('this is winner');
}, 4);
});
var loserPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is loser');
resolve('this is loser');
}, 1000);
});
// 第一個promise變?yōu)閞esolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
console.log(value);// => 'this is winner'
});
我們可以將剛才的 delayPromise 和其它 promise 對象一起放到 Promise.race 中來是實現(xiàn)簡單的超時機制。
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
throw new Error('Operation timed out after ' + ms + ' ms');
});
return Promise.race([promise, timeout]);
}
函數(shù) timeoutPromise(比較對象 promise, ms) 接收兩個參數(shù),第一個是需要使用超時機制的 promise 對象,第二個參數(shù)是超時時間,它返回一個由 Promise.race 創(chuàng)建的相互競爭的 promise 對象。
之后我們就可以使用 timeoutPromise 編寫下面這樣的具有超時機制的代碼了。
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
throw new Error('Operation timed out after ' + ms + ' ms');
});
return Promise.race([promise, timeout]);
}
// 運行示例
var taskPromise = new Promise(function(resolve){
// 隨便一些什么處理
var delay = Math.random() * 2000;
setTimeout(function(){
resolve(delay + "ms");
}, delay);
});
timeoutPromise(taskPromise, 1000).then(function(value){
console.log("taskPromise在規(guī)定時間內(nèi)結(jié)束 : " + value);
}).catch(function(error){
console.log("發(fā)生超時", error);
});
雖然在發(fā)生超時的時候拋出了異常,但是這樣的話我們就不能區(qū)分這個異常到底是普通的錯誤還是超時錯誤了。
為了能區(qū)分這個 Error 對象的類型,我們再來定義一個 Error 對象的子類 TimeoutError。
Error 對象是 ECMAScript 的內(nèi)建(build in)對象。
但是由于 stack trace 等原因我們不能完美的創(chuàng)建一個繼承自 Error 的類,不過在這里我們的目的只是為了和 Error 有所區(qū)別,我們將創(chuàng)建一個 TimeoutError 類來實現(xiàn)我們的目的。
在 ECMAScript6 中可以使用 class 語法來定義類之間的繼承關(guān)系。
class MyError extends Error{
// 繼承了Error類的對象
}
為了讓我們的 TimeoutError 能支持類似 error instanceof TimeoutError 的使用方法,我們還需要進行如下工作。
function copyOwnFrom(target, source) {
Object.getOwnPropertyNames(source).forEach(function (propName) {
Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
});
return target;
}
function TimeoutError() {
var superInstance = Error.apply(null, arguments);
copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error.prototype);
TimeoutError.prototype.constructor = TimeoutError;
我們定義了 TimeoutError 類和構(gòu)造函數(shù),這個類繼承了 Error 的 prototype。
它的使用方法和普通的 Error 對象一樣,使用 throw 語句即可,如下所示。
var promise = new Promise(function(){
throw TimeoutError("timeout");
});
promise.catch(function(error){
console.log(error instanceof TimeoutError);// true
});
有了這個 TimeoutError 對象,我們就能很容易區(qū)分捕獲的到底是因為超時而導(dǎo)致的錯誤,還是其他原因?qū)е碌?Error 對象了。
本章里介紹的繼承 JavaScript 內(nèi)建對象的方法可以參考 Chapter 28. Subclassing Built-ins ,那里有詳細的說明。此外 Error - JavaScript | MDN 也針對 Error 對象進行了詳細說明。
到這里,我想各位讀者都已經(jīng)對如何使用 Promise 來取消一個 XHR 請求都有一些思路了吧。
取消 XHR 操作本身的話并不難,只需要調(diào)用 XMLHttpRequest 對象的 abort() 方法就可以了。
為了能在外部調(diào)用 abort() 方法,我們先對之前本節(jié)出現(xiàn)的 getURL 進行簡單的擴展,cancelableXHR 方法除了返回一個包裝了 XHR 的 promise 對象之外,還返回了一個用于取消該 XHR 請求的 abort 方法。
function cancelableXHR(URL) {
var req = new XMLHttp