鍍金池/ 教程/ HTML/ Advanced
Promise 測(cè)試
實(shí)戰(zhàn) Promise
用語集
前言
什么是 Promise
API Reference
Advanced

Advanced

在這一章里,我們會(huì)基于前面學(xué)到的內(nèi)容,再深入了解一下 Promise 里的一些高級(jí)內(nèi)容,加深對(duì) Promise 的理解。

4.1. Promise 的實(shí)現(xiàn)類庫(kù)(Library)

在本小節(jié)里,我們將不打算對(duì)瀏覽器實(shí)現(xiàn)的 Promise 進(jìn)行說明,而是要介紹一些第三方實(shí)現(xiàn)的和 Promise 兼容的類庫(kù)。

4.1.1. 為什么需要這些類庫(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.allcatch 等功能。

但是 Promises/A+ 實(shí)際上只是定義了關(guān)于 Promise#then 的規(guī)范,所以有些類庫(kù)可能實(shí)現(xiàn)了其它諸如 allcatch 等功能,但是可能名字卻不一樣。

如果我們說一個(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ò)誤。

4.1.2. Polyfill和擴(kuò)展類庫(kù)

在這些 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à)值。

4.1.3. 總結(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)用自如,這也是本書的寫作目的之一。

4.2. Promise.resolve 和 Thenable

在 第二章的 Promise.resolve 中我們已經(jīng)說過, Promise.resolve 的最大特征之一就是可以將 thenable 的對(duì)象轉(zhuǎn)換為 promise 對(duì)象。

在本小節(jié)里,我們將學(xué)習(xí)一下利用將 thenable 對(duì)象轉(zhuǎn)換為 promise 對(duì)象這個(gè)功能都能具體做些什么事情。

4.2.1. 將 Web Notifications 轉(zhuǎn)換為 thenable 對(duì)象

這里我們以桌面通知 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);
    });

到用戶收到并顯示通知為止,整體的處理流程如下所示。

  • 顯示是否允許通知的對(duì)話框,并異步處理用戶選擇結(jié)果
  • 如果用戶允許的話,則通過 new Notification 顯示通知消息。這又分兩種情況
    • 用戶之前已經(jīng)允許過
    • 當(dāng)場(chǎng)彈出是否允許桌面通知對(duì)話框
  • 當(dāng)用戶不允許的時(shí)候,不執(zhí)行任何操作

雖然上面說到了幾種情景,但是最終結(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)格的代碼入手看看到底怎么去做。

4.2.2. Web Notification 包裝函數(shù)(wrapper)

首先,我們以回到函數(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

  • 在類庫(kù)中實(shí)現(xiàn) Promise 功能
  • 例如) localForage

在回調(diào)函數(shù)中也應(yīng)該能夠使用 Promise

  • 用戶可以選擇合適的使用方式
  • 返回 Thenable 類型

notification-as-promise.js 就是以 Promise 存在為前提的寫法。

回歸正文,在這里 Thenable 是為了幫助實(shí)現(xiàn)在回調(diào)函數(shù)中也能使用 Promise 的一個(gè)概念。

4.2.4. Web Notifications As Thenable

我們已經(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.jsnotification-as-promise.js 比起來,有以下的不同點(diǎn)。

  • 類庫(kù)側(cè)沒有提供 Promise 的實(shí)現(xiàn)

    • 用戶通過 Promise.resolve(thenable) 來自己實(shí)現(xiàn)了 Promise
  • 作為 Promise 使用的時(shí)候,需要和 Promise.resolve(thenable) 一起配合使用

4.2.5. 總結(jié)

在本小節(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è)非常重要的概念。

4.3. 使用 reject 而不是 throw

Promise 的構(gòu)造函數(shù),以及被 then 調(diào)用執(zhí)行的函數(shù)基本上都可以認(rèn)為是在 try...catch 代碼塊中執(zhí)行的,所以在這些代碼中即使使用 throw ,程序本身也不會(huì)因?yàn)楫惓6K止。

如果在 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ì)象也就很好理解了。

4.3.1. 使用reject有什么優(yōu)點(diǎn)?

話說回來,為什么在想將 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)重的影響了瀏覽器提供的此功能的正常使用。

4.3.2. 在 then 中進(jìn)行 reject

在 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ì)傳給后面的 thencatch 中的回調(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);

4.3.3. 總結(jié)

在本小節(jié)我們主要學(xué)習(xí)了

  • 使用 reject 會(huì)比使用 throw 安全

  • then 中使用 reject 的方法

也許實(shí)際中我們可能不常使用 reject ,但是比起來不假思索的使用 throw 來說,使用 reject 的好處還是很多的。

關(guān)于上面講的內(nèi)容的比較詳細(xì)的例子,大家可以參考在 使用 Promise.race 和 delay 取消 XHR 請(qǐng)求 小節(jié)的介紹。

4.4. Deferred 和 Promise

這一節(jié)我們來簡(jiǎn)單介紹下 Deferred 和 Promise 之間的關(guān)系

4.4.1. 什么是 Deferred?

說起 Promise ,我想大家一定同時(shí)也聽說過 Deferred 這個(gè)術(shù)語。比如 jQuery.DeferredJSDeferred 等,一定都是大家非常熟悉的內(nèi)容了。

Deferred 和 Promise不同,它沒有共通的規(guī)范,每個(gè) Library 都是根據(jù)自己的喜好來實(shí)現(xiàn)的。

在這里,我們打算以 jQuery.Deferred 類似的實(shí)現(xiàn)為中心進(jìn)行介紹。

4.4.2. Deferred 和Promise 的關(guān)系

簡(jiǎn)單來說,Deferred 和 Promise 具有如下的關(guān)系。

  • Deferred 擁有 Promise

  • Deferred 具備對(duì) Promise 的狀態(tài)進(jìn)行操作的特權(quán)方法(圖中的"特権メソッド")

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。

4.4.3. Deferred top on Promise

基于 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 括起來

    • 由于沒有被嵌套在函數(shù)中,可以減少一層縮進(jìn)
    • 反過來沒有 Promise 里的錯(cuò)誤處理邏輯

在以下方面,它們則完成了同樣的工作。

  • 整體處理流程
    • 調(diào)用 resolve、reject 的時(shí)機(jī)
    • 函數(shù)都返回了 promise 對(duì)象

由于 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ì) resolvereject 方法進(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 引入的。

4.5. 使用 Promise.race 和 delay 取消 XHR 請(qǐng)求

在本小節(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)行中的操作。

4.5.1. 讓 Promise 等待指定時(shí)間

首先我們來看一下如何在 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)切記。

4.5.2. Promise.race 中的超時(shí)

讓我們回顧一下靜態(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。

4.5.3. 定制 Error 對(duì)象

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ì)象
 }

為了讓我們的 TimeoutError 能支持類似 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ì)說明。

4.5.4. 通過超時(shí)取消 XHR 操作

到這里,我想各位讀者都已經(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