Promise.prototype.finally() 最近達到了 TC39 提案的 第 4 階段 。這意味著 Promise.prototype.finally() 提案被採納成為 ECMAScript 最新特性草案 的一部分,登陸 Node.js 現在只是時間問題了。這篇文章會向大家展示 Promise.prototype.finally() 的用法和簡化版 Polyfill 的寫法。
Promise.prototype.finally() 是什麼?
假設你建立了一個新的 Promise:
1 2 3 4 5 6 7 8 9 10 11 |
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 串聯在一起。
1 2 |
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。
1 |
promiseThatRejects.catch(() => console.log('Will print after about 1 second')); |
.catch() 函式只是一個只有 onRejected() 引數的 .then() 的語法糖:
1 2 3 |
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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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’ 。
1 2 3 4 5 6 7 8 9 10 |
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’。
1 2 3 4 5 6 7 8 9 10 |
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 錯誤警告。
1 2 3 4 5 6 7 8 9 10 |
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')); |
1 2 3 4 5 |
$ 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() 後面被呼叫。
1 2 3 4 5 6 7 8 9 10 11 12 |
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() 以處理可能發生的錯誤。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 返回值被忽略,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 的實現。
1 2 3 4 5 6 7 8 9 |
// 向 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() 來在函式完成時釋放資源的鎖定。