4.2. Promise.resolve和Thenable

第二章的Promise.resolve 中我们已经说过, Promise.resolve 的最大特征之一就是可以将thenable的对象转换为promise对象。

在本小节里,我们将学习一下利用将thenable对象转换为promise对象这个功能都能具体做些什么事情。

4.2.1. 将Web Notifications转换为thenable对象

这里我们以桌面通知 API Web Notifications 为例进行说明。

关于Web Notifications API的详细信息可以参考下面的网址。

简单来说,Web Notifications API就是能像以下代码那样通过 new Notification 来显示通知消息。

  1. new Notification("Hi!");

当然,为了显示通知消息,我们需要在运行 new Notification 之前,先获得用户的许可。

确认是否允许Notification的对话框

Figure 11. 确认是否允许Notification的对话框

用户在这个是否允许Notification的对话框选择后的结果,会通过 Notification.permission 传给我们的程序,它的值可能是允许(“granted”)或拒绝(“denied”)这二者之一。

是否允许Notification对话框中的可选项,在Firefox中除了允许、拒绝之外,还增加了 永久有效会话范围内有效 两种额外选项,当然 Notification.permission 的值都是一样的。

在程序中可以通过 Notification.requestPermission() 来弹出是否允许Notification对话框, 用户选择的结果会通过 status 参数传给回调函数。

从这个回调函数我们也可以看出来,用户选择允许还是拒绝通知是异步进行的。

  1. Notification.requestPermission(function (status) {
  2. // status的值为 "granted" 或 "denied"
  3. console.log(status);
  4. });

到用户收到并显示通知为止,整体的处理流程如下所示。

  • 显示是否允许通知的对话框,并异步处理用户选择结果

  • 如果用户允许的话,则通过 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

  1. function notifyMessage(message, options, callback) {
  2. if (Notification && Notification.permission === 'granted') {
  3. var notification = new Notification(message, options);
  4. callback(null, notification);
  5. } else if (Notification.requestPermission) {
  6. Notification.requestPermission(function (status) {
  7. if (Notification.permission !== status) {
  8. Notification.permission = status;
  9. }
  10. if (status === 'granted') {
  11. var notification = new Notification(message, options);
  12. callback(null, notification);
  13. } else {
  14. callback(new Error('user denied'));
  15. }
  16. });
  17. } else {
  18. callback(new Error('doesn\'t support Notification API'));
  19. }
  20. }
  21. // 运行实例
  22. // 第二个参数是传给 `Notification` 的option对象
  23. notifyMessage("Hi!", {}, function (error, notification) {
  24. if(error){
  25. return console.error(error);
  26. }
  27. console.log(notification);// 通知对象
  28. });

在回调风格的代码里,当用户拒绝接收通知的时候, error 会被设置值,而如果用户同意接收通知的时候,则会显示通知消息并且 notification 会被设置值。

回调函数接收error和notification两个参数

  1. function callback(error, notification){
  2. }

下面,我想再将这个回调函数风格的代码使用Promise进行改写。

4.2.3. Web Notification as Promise

基于上述回调风格的 notifyMessage 函数,我们再来创建一个返回promise对象的 notifyMessageAsPromise 方法。

notification-as-promise.js

  1. function notifyMessage(message, options, callback) {
  2. if (Notification && Notification.permission === 'granted') {
  3. var notification = new Notification(message, options);
  4. callback(null, notification);
  5. } else if (Notification.requestPermission) {
  6. Notification.requestPermission(function (status) {
  7. if (Notification.permission !== status) {
  8. Notification.permission = status;
  9. }
  10. if (status === 'granted') {
  11. var notification = new Notification(message, options);
  12. callback(null, notification);
  13. } else {
  14. callback(new Error('user denied'));
  15. }
  16. });
  17. } else {
  18. callback(new Error('doesn\'t support Notification API'));
  19. }
  20. }
  21. function notifyMessageAsPromise(message, options) {
  22. return new Promise(function (resolve, reject) {
  23. notifyMessage(message, options, function (error, notification) {
  24. if (error) {
  25. reject(error);
  26. } else {
  27. resolve(notification);
  28. }
  29. });
  30. });
  31. }
  32. // 运行示例
  33. notifyMessageAsPromise("Hi!").then(function (notification) {
  34. console.log(notification);// 通知对象
  35. }).catch(function(error){
  36. console.error(error);
  37. });

在用户允许接收通知的时候,运行上面的代码,会显示 "Hi!" 消息。

当用户接收通知消息的时候, .then 函数会被调用,当用户拒绝接收消息的时候, .catch 方法会被调用。

由于浏览器是以网站为单位保存Web Notifications API的许可状态的,所以实际上有下面四种模式存在。

    已经获得用户许可

    .then 方法被调用

    弹出询问对话框并获得许可

    .then 方法被调用

    已经是被用户拒绝的状态

    .catch 方法被调用

    弹出询问对话框并被用户拒绝

    .catch 方法被调用

也就是说,如果使用原生的Web Notifications API的话,那么需要在程序中对上述四种情况都进行处理,我们可以像下面的包装函数那样,将上述四种情况简化为两种以方便处理。

上面的 notification-as-promise.js 虽然看上去很方便,但是实际上使用的时候,很可能出现 在不支持Promise的环境下不能使用 的问题。

如果你想编写像notification-as-promise.js这样具有Promise风格和的类库的话,我觉得你有如下的一些选择。

支持Promise的环境是前提

  • 需要最终用户保证支持Promise

  • 在不支持Promise的环境下不能正常工作(即应该出错)。

在类库中实现Promise

  • 在类库中实现Promise功能

  • 例如) localForage

在回调函数中也应该能够使用 Promise

  • 用户可以选择合适的使用方式

  • 返回Thenable类型

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

回归正文,在这里Thenable是为了帮助实现在回调函数中也能使用Promise的一个概念。

4.2.4. Web Notifications As Thenable

我们已经说过,thenable就是一个具有 .then方法的一个对象。下面我们就在notification-callback.js中增加一个返回值为 thenable 类型的方法。

notification-thenable.js

  1. function notifyMessage(message, options, callback) {
  2. if (Notification && Notification.permission === 'granted') {
  3. var notification = new Notification(message, options);
  4. callback(null, notification);
  5. } else if (Notification.requestPermission) {
  6. Notification.requestPermission(function (status) {
  7. if (Notification.permission !== status) {
  8. Notification.permission = status;
  9. }
  10. if (status === 'granted') {
  11. var notification = new Notification(message, options);
  12. callback(null, notification);
  13. } else {
  14. callback(new Error('user denied'));
  15. }
  16. });
  17. } else {
  18. callback(new Error('doesn\'t support Notification API'));
  19. }
  20. }
  21. // 返回 `thenable`
  22. function notifyMessageAsThenable(message, options) {
  23. return {
  24. 'then': function (resolve, reject) {
  25. notifyMessage(message, options, function (error, notification) {
  26. if (error) {
  27. reject(error);
  28. } else {
  29. resolve(notification);
  30. }
  31. });
  32. }
  33. };
  34. }
  35. // 运行示例
  36. Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
  37. console.log(notification);// 通知对象
  38. }).catch(function(error){
  39. console.error(error);
  40. });

notification-thenable.js里增加了一个 notifyMessageAsThenable方法。这个方法返回的对象具备一个then方法。

then方法的参数和 new Promise(function (resolve, reject){}) 一样,在确定时执行 resolve 方法,拒绝时调用 reject 方法。

then 方法和 notification-as-promise.js 中的 notifyMessageAsPromise 方法完成了同样的工作。

我们可以看出, Promise.resolve(thenable) 通过使用了 thenable 这个promise对象,就能利用Promise功能了。

  1. Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
  2. console.log(notification);// 通知对象
  3. }).catch(function(error){
  4. console.error(error);
  5. });

使用了Thenable的notification-thenable.js 和依赖于Promise的 notification-as-promise.js ,实际上都是非常相似的使用方法。

notification-thenable.jsnotification-as-promise.js比起来,有以下的不同点。

  • 类库侧没有提供 Promise 的实现

    • 用户通过 Promise.resolve(thenable) 来自己实现了 Promise
  • 作为Promise使用的时候,需要和 Promise.resolve(thenable) 一起配合使用

通过使用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类库 之间进行相互转换了。

比如,类库Q的Promise实例为Q promise对象,提供了 ES6 Promises 的promise对象不具备的方法。Q promise对象提供了 promise.finally(callback)promise.nodeify(callback) 等方法。

如果你想将ES6 Promises的promise对象转换为Q promise的对象,轮到Thenable大显身手的时候就到了。

使用thenable将promise对象转换为Q promise对象

  1. var Q = require("Q");
  2. // 这是一个ES6的promise对象
  3. var promise = new Promise(function(resolve){
  4. resolve(1);
  5. });
  6. // 变换为Q promise对象
  7. Q(promise).then(function(value){
  8. console.log(value);
  9. }).finally(function(){ (1)
  10. console.log("finally");
  11. });
1因为是Q promise对象所以可以使用 finally 方法

上面代码中最开始被创建的promise对象具备then方法,因此是一个Thenable对象。我们可以通过Q(thenable)方法,将这个Thenable对象转换为Q promise对象。

可以说它的机制和 Promise.resolve(thenable) 一样,当然反过来也一样。

像这样,Promise类库虽然都有自己类型的promise对象,但是它们之间可以通过Thenable这个共通概念,在类库之间(当然也包括native Promise)进行promise对象的相互转换。

我们看到,就像上面那样,Thenable多在类库内部实现中使用,所以从外部来说不会经常看到Thenable的使用。但是我们必须牢记Thenable是Promise中一个非常重要的概念。