在?[第二章的Promise.resolve](http://liubin.github.io/promises-book/#ch2-promise-resolve)?中我們已經說過,?`Promise.resolve`?的最大特征之一就是可以將thenable的對象轉換為promise對象。
在本小節里,我們將學習一下利用將thenable對象轉換為promise對象這個功能都能具體做些什么事情。
## 4.2.1\. 將Web Notifications轉換為thenable對象
這里我們以桌面通知 API?[Web Notifications](https://developer.mozilla.org/ja/docs/Web/API/notification)?為例進行說明。
關于Web Notifications API的詳細信息可以參考下面的網址。
* [使用 Web Notifications - WebAPI | MDN](https://developer.mozilla.org/zh-TW/docs/WebAPI/Using_Web_Notifications)
* [Can I use Web Notifications](http://caniuse.com/notifications)
簡單來說,Web Notifications API就是能像以下代碼那樣通過?`new Notification`?來顯示通知消息。
~~~
new Notification("Hi!");
~~~
當然,為了顯示通知消息,我們需要在運行?`new Notification`?之前,先獲得用戶的許可。

Figure 11\. 確認是否允許Notification的對話框
用戶在這個是否允許Notification的對話框選擇后的結果,會通過?`Notification.permission`?傳給我們的程序,它的值可能是允許("granted")或拒絕("denied")這二者之一。
> 是否允許Notification對話框中的可選項,在Firefox中除了允許、拒絕之外,還增加了?_永久有效_?和?_會話范圍內有效_?兩種額外選項,當然?`Notification.permission`?的值都是一樣的。
在程序中可以通過?`Notification.requestPermission()`?來彈出是否允許Notification對話框, 用戶選擇的結果會通過?`status`?參數傳給回調函數。
從這個回調函數我們也可以看出來,用戶選擇允許還是拒絕通知是異步進行的。
~~~
Notification.requestPermission(function (status) {
// status的值為 "granted" 或 "denied"
console.log(status);
});
~~~
到用戶收到并顯示通知為止,整體的處理流程如下所示。
* 顯示是否允許通知的對話框,并異步處理用戶選擇結果
* 如果用戶允許的話,則通過?`new Notification`?顯示通知消息。這又分兩種情況
* 用戶之前已經允許過
* 當場彈出是否允許桌面通知對話框
* 當用戶不允許的時候,不執行任何操作
雖然上面說到了幾種情景,但是最終結果就是用戶允許或者拒絕,可以總結為如下兩種模式。
允許時("granted")
使用?`new Notification`?創建通知消息
拒絕時("denied")
沒有任何操作
這兩種模式是不是覺得有在哪里看過的感覺? 呵呵,用戶的選擇結果,正和在Promise中promise對象變為 Fulfilled 或 Rejected 狀態非常類似。
resolve(成功)時 == 用戶允許("granted")
調用?`onFulfilled`?方法
reject(失敗)時 == 用戶拒絕("denied")
調用?`onRejected`?函數
是不是我們可以用Promise的方式去編寫桌面通知的代碼呢?我們先從回調函數風格的代碼入手看看到底怎么去做。
## 4.2.2\. Web Notification 包裝函數(wrapper)
首先,我們以回到函數風格的代碼對上面的Web Notification API包裝函數進行重寫,新代碼如下所示。
notification-callback.js
~~~
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'));
}
}
// 運行實例
// 第二個參數是傳給 `Notification` 的option對象
notifyMessage("Hi!", {}, function (error, notification) {
if(error){
return console.error(error);
}
console.log(notification);// 通知對象
});
~~~
在回調風格的代碼里,當用戶拒絕接收通知的時候,?`error`?會被設置值,而如果用戶同意接收通知的時候,則會顯示通知消息并且?`notification`?會被設置值。
回調函數接收error和notification兩個參數
~~~
function callback(error, notification){
}
~~~
下面,我想再將這個回調函數風格的代碼使用Promise進行改寫。
## 4.2.3\. Web Notification as Promise
基于上述回調風格的?`notifyMessage`?函數,我們再來創建一個返回promise對象的?`notifyMessageAsPromise`?方法。
notification-as-promise.js
~~~
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!"`?消息。
當用戶接收通知消息的時候,?`.then`?函數會被調用,當用戶拒絕接收消息的時候,?`.catch`?方法會被調用。
> 由于瀏覽器是以網站為單位保存Web Notifications API的許可狀態的,所以實際上有下面四種模式存在。
> 已經獲得用戶許可
> `.then`?方法被調用
> 彈出詢問對話框并獲得許可
> `.then`?方法被調用
> 已經是被用戶拒絕的狀態
> `.catch`?方法被調用
> 彈出詢問對話框并被用戶拒絕
> `.catch`?方法被調用
> 也就是說,如果使用原生的Web Notifications API的話,那么需要在程序中對上述四種情況都進行處理,我們可以像下面的包裝函數那樣,將上述四種情況簡化為兩種以方便處理。
上面的?[notification-as-promise.js](http://liubin.github.io/promises-book/#notification-as-promise.js)?雖然看上去很方便,但是實際上使用的時候,很可能出現?**在不支持Promise的環境下不能使用**?的問題。
如果你想編寫像[notification-as-promise.js](http://liubin.github.io/promises-book/#notification-as-promise.js)這樣具有Promise風格和的類庫的話,我覺得你有如下的一些選擇。
支持Promise的環境是前提
* 需要最終用戶保證支持`Promise`
* 在不支持Promise的環境下不能正常工作(即應該出錯)。
在類庫中實現`Promise`
* 在類庫中實現`Promise`功能
* 例如)?[localForage](https://github.com/mozilla/localForage)
在回調函數中也應該能夠使用?`Promise`
* 用戶可以選擇合適的使用方式
* 返回Thenable類型
[notification-as-promise.js](http://liubin.github.io/promises-book/#notification-as-promise.js)就是以`Promise`存在為前提的寫法。
回歸正文,在這里[Thenable](http://liubin.github.io/promises-book/#Thenable)是為了幫助實現**在回調函數中也能使用`Promise`**的一個概念。
## 4.2.4\. Web Notifications As Thenable
我們已經說過,[thenable](http://liubin.github.io/promises-book/#Thenable)就是一個具有?`.then`方法的一個對象。下面我們就在[notification-callback.js](http://liubin.github.io/promises-book/#notification-callback.js)中增加一個返回值為?`thenable`?類型的方法。
notification-thenable.js
~~~
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](http://liubin.github.io/promises-book/#notification-thenable.js)里增加了一個?`notifyMessageAsThenable`方法。這個方法返回的對象具備一個`then`方法。
`then`方法的參數和?`new Promise(function (resolve, reject){})`?一樣,在確定時執行?`resolve`?方法,拒絕時調用?`reject`?方法。
`then`?方法和?[notification-as-promise.js](http://liubin.github.io/promises-book/#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](http://liubin.github.io/promises-book/#notification-thenable.js)?和依賴于Promise的?[notification-as-promise.js](http://liubin.github.io/promises-book/#notification-as-promise.js)?,實際上都是非常相似的使用方法。
[notification-thenable.js](http://liubin.github.io/promises-book/#notification-thenable.js)?和?[notification-as-promise.js](http://liubin.github.io/promises-book/#notification-as-promise.js)比起來,有以下的不同點。
* 類庫側沒有提供?`Promise`?的實現
* 用戶通過?`Promise.resolve(thenable)`?來自己實現了?`Promise`
* 作為Promise使用的時候,需要和?`Promise.resolve(thenable)`?一起配合使用
通過使用[Thenable](http://liubin.github.io/promises-book/#Thenable)對象,我們可以實現類似已有的回調式風格和Promise風格中間的一種實現風格。
## 4.2.5\. 總結
在本小節我們主要學習了什么是Thenable,以及如何通過`Promise.resolve(thenable)`?使用Thenable,將其作為promise對象來使用。
Callback?—?Thenable?—?Promise
Thenable風格表現為位于回調和Promise風格中間的一種狀態,作為類庫的公開API有點不太成熟,所以并不常見。
Thenable本身并不依賴于`Promise`功能,但是Promise之外也沒有使用Thenable的方式,所以可以認為Thenable間接依賴于Promise。
另外,用戶需要對?`Promise.resolve(thenable)`?有所理解才能使用好Thenable,因此作為類庫的公開API有一部分會比較難。和公開API相比,更多情況下是在內部使用Thenable。
> 在編寫異步處理的類庫的時候,推薦采用先編寫回調風格的函數,然后再轉換為公開API這種方式。
> 貌似Node.js的Core module就采用了這種方式,除了類庫提供的基本回調風格的函數之外,用戶也可以通過Promise或者Generator等自己擅長的方式進行實現。
> 最初就是以能被Promise使用為目的的類庫,或者其本身依賴于Promise等情況下,我想將返回promise對象的函數作為公開API應該也沒什么問題。
### 什么時候該使用Thenable?
那么,又是在什么情況下應該使用Thenable呢?
恐怕最可能被使用的是在?[Promise類庫](http://liubin.github.io/promises-book/#promise-library)?之間進行相互轉換了。
比如,類庫Q的Promise實例為Q promise對象,提供了?[ES6 Promises](http://liubin.github.io/promises-book/#es6-promises)?的promise對象不具備的方法。Q promise對象提供了?`promise.finally(callback)`?和?`promise.nodeify(callback)`?等方法。
如果你想將ES6 Promises的promise對象轉換為Q promise的對象,輪到Thenable大顯身手的時候就到了。
使用thenable將promise對象轉換為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(){ //因為是Q promise對象所以可以使用?`finally`?方法
console.log("finally");
});
~~~
上面代碼中最開始被創建的promise對象具備`then`方法,因此是一個Thenable對象。我們可以通過`Q(thenable)`方法,將這個Thenable對象轉換為Q promise對象。
可以說它的機制和?`Promise.resolve(thenable)`?一樣,當然反過來也一樣。
像這樣,Promise類庫雖然都有自己類型的promise對象,但是它們之間可以通過Thenable這個共通概念,在類庫之間(當然也包括native Promise)進行promise對象的相互轉換。
我們看到,就像上面那樣,Thenable多在類庫內部實現中使用,所以從外部來說不會經常看到Thenable的使用。但是我們必須牢記Thenable是Promise中一個非常重要的概念。
- 前言
- 第一章 - 什么是Promise
- 1.1. 什么是Promise
- 1.2. Promise簡介
- 1.3. 編寫Promise代碼
- 第二章 - 實戰Promise
- 2.1. Promise.resolve
- 2.2. Promise.reject
- 2.3. 專欄: Promise只能進行異步操作?
- 2.4. Promise#then
- 2.5. Promise#catch
- 2.6. 專欄: 每次調用then都會返回一個新創建的promise對象
- 2.7. Promise和數組
- 2.8. Promise.all
- 2.9. Promise.race
- 2.10. then or catch?
- 第三章 - Promise測試
- 3.1. 基本測試
- 3.2. Mocha對Promise的支持
- 3.3. 編寫可控測試(controllable tests)
- 第四章 - Advanced
- 4.1. Promise的實現類庫(Library)
- 4.2. Promise.resolve和Thenable
- 4.3. 使用reject而不是throw
- 4.4. Deferred和Promise
- 4.5. 使用Promise.race和delay取消XHR請求
- 4.6. 什么是 Promise.prototype.done ?
- 4.7. Promise和方法鏈(method chain)
- 4.8. 使用Promise進行順序(sequence)處理
- 第五章 - Promises API Reference
- 5.1. Promise#then
- 5.2. Promise#catch
- 5.3. Promise.resolve
- 5.4. Promise.reject
- 5.5. Promise.all
- 5.6. Promise.race
- 第六章 - 用語集
- 第七章 - 參考網站
- 第八章 - 關于作者
- 第九章 - 關于譯者