AlekoLau

在 Node.js 中使用 Promise.prototype.finally

原文链接: thecodebarbarian.com

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() 来在函数完成时释放资源的锁定。