4.2. Promise.resolve和Thenable

最后更新于:2022-04-01 21:11:04

在 [第二章的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` 之前,先获得用户的许可。 ![确认是否允许Notification的对话框](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-07-20_55ac7a3248291.png) 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中一个非常重要的概念。
';