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