在 Node.js 中使用 Promise.prototype.finally
Promise.prototype.finally() 最近达到了 TC39 提案的 第 4 阶段 。这意味着 Promise.prototype.finally()
提案被采纳成为 ECMAScript 最新特性草案 的一部分,登陆 Node.js 现在只是时间问题了。这篇文章会向大家展示 Promise.prototype.finally()
的用法和简化版 Polyfill 的写法。
Promise.prototype.finally() 是什么?
假设你创建了一个新的 Promise
:
const promiseThatFulfills = new Promise((resolve) => { // 调用 resolve() 可以让 Promised 的状态变为 fulfilled。"fulfilled" 和 "resolved" 是不同的概念: // 如果你 resolve() 一个非 Promise 值,Promise 会变成 "fulfilled"。 // 然而, 如果 resolve() 一个 Promise,外层(原来的) Promise 会保持 "pending" 状态 // 直到内层 Promise 变为 "fulfilled" 或者 "rejected" setTimeout(() => resolve('Hello, World'), 1000);});const promiseThatRejects = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('whoops!')), 1000);});
你可以用 .then()
函数把这些 Promise
串联在一起。
promiseThatFulfills.then(() => console.log('Will print after about 1 second')); promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));
注意 .then()
需要两个函数作为参数。第一个参数是 onFulfilled()
,当 Promise
为 fulfilled 时调用;第二个 onRejected()
则是在 rejected 的时候调用。Promise
是一个必定处于以下三种状态之一的状态机:
pending(进行中): Promise 中的操作正在进行中,状态未被凝固为 fulfilled 或 rejected。
fulfilled(已完成,直译:已满足): Promise 中的操作已成功完成,现在
Promise
里面关联有该操作的返回值。rejected(已失败,直译:已回绝): Promise 中的操作因某些原因失败,现在
Promise
里面关联有该操作的错误信息。
此外,处于 fulfilled 或者 rejected 状态的 Promise
称作“已凝固”(settled) 的 Promise
。
虽然 .then()
是串联 Promise
的核心机制,但并不独一无二。Promise
用来处理抛出错误的 .catch()
函数 也能串联 Promise
。
promiseThatRejects.catch(() => console.log('Will print after about 1 second'));
.catch()
函数只是一个只有 onRejected()
参数的 .then()
的语法糖:
promiseThatRejects.catch(() => console.log('Will print after about 1 second'));// 等价于 promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));
类似于 .catch()
,.finally()
也是 .then()
的一个语法糖。区别在于 .finally()
当 Promise
凝固(fulfilled / rejected)时执行一个 onFinally
函数。当前 .finally()
还没有加入 Node.js 发行版,但 npm 上的 promise.prototype.finally 模块 实现了它的 Polyfill。
const promiseFinally = require('promise.prototype.finally');// 向 Promise.prototype 增加 finally() promiseFinally.shim(); const promiseThatFulfills = new Promise((resolve) => { setTimeout(() => resolve('Hello, World'), 1000); }); const promiseThatRejects = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('whoops!')), 1000); }); promiseThatFulfills.finally(() => console.log('fulfilled')); promiseThatRejects.finally(() => console.log('rejected'));
上面代码的运行结果会打印 'fulfilled' 和 'rejected',因为无论是 fulfilled 还是 rejected,只要状态凝固 onFinally
都会立即执行。不过 onFinally
不接受参数,所以你无法判断 Promise
的状态到底是两个中的哪个。
finally()
会返回一个 Promise
,所以你可以使用 .then()
/ .catch()
/ .finally()
串联它的返回值。finally() 返回的 Promise
会和它连接到的 Promise
保持相同的 fulfill 条件。 例如下面的代码,即使 onFinally
返回了 'bar',它还是会打印 5 次 'foo' 。
const promiseFinally = require('promise.prototype.finally');// 向 Promise.prototype 增加 finally() promiseFinally.shim(); Promise.resolve('foo'). finally(() => 'bar'). // 会打印 'foo', **不是** 'bar',因为 finally() 只起到转运的作用 // for fulfilled values and rejected errors then(res => console.log(res));
类似地,下面代码中即使 onFinally
没有抛出任何错误,仍然会打印 'foo'。
const promiseFinally = require('promise.prototype.finally');// 向 Promise.prototype 增加 finally()promiseFinally.shim();Promise.reject(new Error('foo')). finally(() => 'bar'). // 会打印 'foo', **不是** 'bar',因为 finally() 只起到转运的作用 // 无论是 resolve 的值还是 reject 的错误 catch(err => console.log(err.message));
上面代码展示了使用 finally()
的一个重要细节:它 不会 帮你处理 Promise
的错误。如何让它能处理 Promise
错误值得更深入的研究。
错误处理
finally()
不是 用来处理 Promise
的错误的。事实上,它会在 onFinally()
执行后显式重新抛错。下面的代码会打印一个未被处理的 Promise
错误警告。
const promiseFinally = require('promise.prototype.finally');// 向 Promise.prototype 增加 finally() promiseFinally.shim(); const promiseThatRejects = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('whoops!')), 1000); }); promiseThatRejects.finally(() => console.log('rejected'));
$ node finally.js rejected (node:5342) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: whoops! (node:5342) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. $
与 try
/catch
/finally
类似,通常 .finally()
都会在 .catch()
后面被调用。
const promiseFinally = require('promise.prototype.finally');// 向 Promise.prototype 增加 finally()promiseFinally.shim();const promiseThatRejects = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('whoops!')), 1000); }); promiseThatRejects. catch(() => { /* ignore the error */ }). finally(() => console.log('done'));
然而 finally()
返回的也是 Promise
,所以你可以随意在 finally()
后面调用 .catch()
。特别地,如果 onFinally
会出错,例如 HTTP 请求,你应该在末尾添加 .catch()
以处理可能发生的错误。
const promiseFinally = require('promise.prototype.finally');// 向 Promise.prototype 增加 finally()promiseFinally.shim();const promiseThatRejects = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('whoops!')), 1000); }); promiseThatRejects. finally(() => console.log('rejected')). // No unhandled promise rejection because there's a .catch() catch(() => { /* ignore the error */ });
简版 Polyfill
我觉得想要真正搞懂一个东西,最简单的方式就是自己去实现一个。.finally()
是一个很好的选择,因为官方 Polyfill 只有 45 行,而且大多数代码在验证原理时可以进一步精简。
接下来是一些关于 .finally()
的测试样例。下面的代码会打印 'foo' 5 次。
// 返回值被忽略,Promise 正常完成Promise.resolve('foo'). finally(() => 'bar'). then(res => console.log(res));// 返回值被忽略,Promise 正常抛错Promise.reject(new Error('foo')). finally(() => 'bar'). catch(err => console.log(err.message));// onFinally 抛错,返回新抛出的错误Promise.reject(new Error('bar')). finally(() => { throw new Error('foo'); }). catch(err => console.log(err.message));// onFinally 返回的是一个抛错的 Promise,// 返回新抛出的错误Promise.reject(new Error('bar')). finally(() => Promise.reject(new Error('foo'))). catch(err => console.log(err.message));// onFinally 返回的是一个 Promise, 需要等待它// 状态凝固才能继续执行const start = Date.now();Promise.resolve('foo'). finally(() => new Promise(resolve => setTimeout(() => resolve(), 1000))). then(res => console.log(res, Date.now() - start));
下面是简版 Polyfill 的实现。
// 向 Promise.prototype 增加 finally()Promise.prototype.finally = function(onFinally) { return this.then( /* onFulfilled */ res => Promise.resolve(onFinally()).then(() => res), /* onRejected */ err => Promise.resolve(onFinally()).then(() => { throw err; }) ); };
这个实现背后关键的思路在于 onFinally
可能返回 Promise
。在这种情况下你需要用 .then()
来处理它并且给外层 Promise
凝固状态。你可以显式检查 onFinally
是否返回 Promise
,但 Promise.resolve()
已经帮你做了,而且不需要 if
语句。你还需要跟踪初始 Promise
的值或错误,并确保 finally()
返回的 Promise
解析出初始值 res
,或重新抛出初始错误 err
。
后记
在动笔时,Promise.prototype.finally()
是 8 个 TC39 第四阶段提案 之一。这意味着 finally()
将和 7 个其他新语言特性一起加入 Node.js。 finally()
是这 8 个新特性中最令人兴奋的之一,皆因为它可以让异步操作结束后的清理更彻底。举个例子,下面我正用在生产环境的代码非常需要 finally()
来在函数完成时释放资源的锁定。
共同学习,写下你的评论
评论加载中...
作者其他优质文章