探索 Node.js 中的 Promise
Promise 是 JavaScript 中的一個特殊物件,表示一個非同步操作的最終完成(或失敗)及其結果值。本質上,Promise 是一個尚未可用但將來會有的值的佔位符。
把 Promise 想象成點披薩:你不會立刻拿到它,但外賣員承諾稍後會送到。你不知道*確切*的時間,但你知道結果要麼是“披薩已送達”,要麼是“出了點問題”。
Promise 的狀態
一個 Promise 可以處於以下三種狀態之一:
- 待定(Pending):初始狀態,非同步操作仍在進行中。
- 已兌現(Fulfilled):操作成功完成,Promise 現在已用一個值解決(resolved)。
- 已拒絕(Rejected):操作失敗,Promise 因一個原因(通常是錯誤)而敲定(settled)。
當你點披薩時,你處於待定狀態,飢餓而充滿希望。如果披薩熱騰騰、香噴噴地送達,你就進入了已兌現狀態。但如果餐廳打電話說他們把你的披薩掉地上了,你就處於已拒絕狀態。
無論你的晚餐是喜是悲,一旦有了最終結果,Promise 就被認為是已敲定(settled)。
Promise 的基本語法
建立 Promise 最常見的方法之一是使用 new Promise() 建構函式。該建構函式接受一個帶有兩個引數的函式:resolve 和 reject。這兩個函式用於將 Promise 從待定狀態轉換到已兌現或已拒絕狀態。
如果在執行器函式內部丟擲錯誤,Promise 將因該錯誤而被拒絕。執行器函式的返回值會被忽略:只應使用 resolve 或 reject 來敲定 Promise。
const = new ((, ) => {
const = true;
if () {
('Operation was successful!');
} else {
('Something went wrong.');
}
});
在上面的例子中:
- 如果
success條件為true,Promise 將被兌現,並將值'Operation was successful!'傳遞給resolve函式。 - 如果
success條件為false,Promise 將被拒絕,並將錯誤'Something went wrong.'傳遞給reject函式。
使用 .then()、.catch() 和 .finally() 處理 Promise
一旦建立了 Promise,你就可以使用 .then()、.catch() 和 .finally() 方法來處理其結果。
.then()用於處理已兌現的 Promise 並訪問其結果。.catch()用於處理已拒絕的 Promise 並捕獲可能發生的任何錯誤。.finally()用於處理已敲定的 Promise,無論 Promise 是解決還是拒絕。
const = new ((, ) => {
const = true;
if () {
('Operation was successful!');
} else {
('Something went wrong.');
}
});
.( => {
.(); // This will run if the Promise is fulfilled
})
.( => {
.(); // This will run if the Promise is rejected
})
.(() => {
.('The promise has completed'); // This will run when the Promise is settled
});
鏈式呼叫 Promise
Promise 的一個強大特性是它們允許你將多個非同步操作連結在一起。當你鏈式呼叫 Promise 時,每個 .then() 塊都會等待前一個塊完成後再執行。
const { : } = ('node:timers/promises');
const = (1000).(() => 'First task completed');
.( => {
.(); // 'First task completed'
return (1000).(() => 'Second task completed'); // Return a second Promise
})
.( => {
.(); // 'Second task completed'
})
.( => {
.(); // If any Promise is rejected, catch the error
});
將 Async/Await 與 Promise 結合使用
在現代 JavaScript 中,處理 Promise 的最佳方式之一是使用 async/await。這讓你能夠編寫看起來像同步程式碼的非同步程式碼,使其更易於閱讀和維護。
async用於定義一個返回 Promise 的函式。await用於在async函式內部暫停執行,直到一個 Promise 敲定。
async function () {
try {
const = await promise1;
.(); // 'First task completed'
const = await promise2;
.(); // 'Second task completed'
} catch () {
.(); // Catches any rejection or error
}
}
();
在 performTasks 函式中,await 關鍵字確保每個 Promise 在繼續執行下一條語句之前都已敲定。這使得非同步程式碼的流程更加線性和易讀。
本質上,上述程式碼的執行效果與使用者編寫以下程式碼相同:
promise1
.then(function () {
.();
return promise2;
})
.then(function () {
.();
})
.catch(function () {
.();
});
頂層 Await
使用 ECMAScript 模組時,模組本身被視為一個原生支援非同步操作的頂層作用域。這意味著你可以在頂層使用 await,而無需 async 函式。
import { as } from 'node:timers/promises';
await (1000);
Async/await 的用法可能比所提供的簡單示例複雜得多。Node.js 技術指導委員會成員 James Snell 有一個深入的演講,探討了 Promise 和 async/await 的複雜性。
基於 Promise 的 Node.js API
Node.js 為其許多核心 API 提供了基於 Promise 的版本,特別是在傳統上使用回撥處理非同步操作的情況下。這使得使用 Node.js API 和 Promise 更加容易,並降低了“回撥地獄”的風險。
例如,fs(檔案系統)模組在 fs.promises 下有一個基於 Promise 的 API:
const = ('node:fs').;
// Or, you can import the promisified version directly:
// const fs = require('node:fs/promises');
async function () {
try {
const = await .('example.txt', 'utf8');
.();
} catch () {
.('Error reading file:', );
}
}
();
在這個例子中,fs.readFile() 返回一個 Promise,我們使用 async/await 語法來非同步讀取檔案內容。
高階 Promise 方法
JavaScript 的 Promise 全域性物件提供了幾個強大的方法,可以幫助更有效地管理多個非同步任務:
Promise.all()
此方法接受一個 Promise 陣列,並返回一個新的 Promise。這個新的 Promise 會在所有 Promise 都兌現後解決。如果任何一個 Promise 被拒絕,Promise.all() 會立即拒絕。然而,即使發生拒絕,其他 Promise 仍會繼續執行。在處理大量 Promise 時,尤其是在批處理中,使用此函式可能會對系統記憶體造成壓力。
const { : } = ('node:timers/promises');
const = (1000).(() => 'Data from API 1');
const = (2000).(() => 'Data from API 2');
.([, ])
.( => {
.(); // ['Data from API 1', 'Data from API 2']
})
.( => {
.('Error:', );
});
Promise.allSettled()
此方法等待所有 promise 都解決或拒絕,並返回一個物件陣列,描述每個 Promise 的結果。
const = .('Success');
const = .('Failed');
.([, ]).( => {
.();
// [ { status: 'fulfilled', value: 'Success' }, { status: 'rejected', reason: 'Failed' } ]
});
與 Promise.all() 不同,Promise.allSettled() 不會在失敗時短路。它會等待所有 promise 都敲定,即使有些被拒絕。這為批處理操作提供了更好的錯誤處理,因為你可能想知道所有任務的狀態,無論成功與否。
Promise.race()
此方法在第一個 Promise 敲定(無論是解決還是拒絕)時立即解決或拒絕。無論哪個 promise 先敲定,所有 promise 都會被完全執行。
const { : } = ('node:timers/promises');
const = (2000).(() => 'Task 1 done');
const = (1000).(() => 'Task 2 done');
.([, ]).( => {
.(); // 'Task 2 done' (since task2 finishes first)
});
Promise.any()
此方法在任意一個 Promise 解決後立即解決。如果所有 promise 都被拒絕,它將以一個 AggregateError 拒絕。
const { : } = ('node:timers/promises');
const = (2000).(() => 'API 1 success');
const = (1000).(() => 'API 2 success');
const = (1500).(() => 'API 3 success');
.([, , ])
.( => {
.(); // 'API 2 success' (since it resolves first)
})
.( => {
.('All promises rejected:', );
});
Promise.reject() 和 Promise.resolve()
這些方法直接建立一個已拒絕或已解決的 Promise。
.('Resolved immediately').( => {
.(); // 'Resolved immediately'
});
Promise.try()
Promise.try() 是一個執行給定函式的方法,無論該函式是同步還是非同步,並將其結果包裝在一個 promise 中。如果函式丟擲錯誤或返回一個被拒絕的 promise,Promise.try() 將返回一個被拒絕的 promise。如果函式成功完成,返回的 promise 將以其值兌現。
這對於以一致的方式啟動 promise 鏈特別有用,尤其是在處理可能同步丟擲錯誤的程式碼時。
function () {
if (.() > 0.5) {
throw new ('Oops, something went wrong!');
}
return 'Success!';
}
.()
.( => {
.('Result:', );
})
.( => {
.('Caught error:', .message);
});
在這個例子中,Promise.try() 確保如果 mightThrow() 丟擲錯誤,它將在 .catch() 塊中被捕獲,從而更容易地在一個地方處理同步和非同步錯誤。
Promise.withResolvers()
此方法建立一個新的 promise 及其關聯的 resolve 和 reject 函式,並將它們返回在一個方便的物件中。例如,當你需要建立一個 promise,但稍後從執行器函式外部解決或拒絕它時,可以使用此方法。
const { , , } = .();
(() => {
('Resolved successfully!');
}, 1000);
.( => {
.('Success:', );
});
在此示例中,Promise.withResolvers() 讓你完全控制 promise 何時以及如何被解決或拒絕,而無需內聯定義執行器函式。這種模式常用於事件驅動程式設計、超時或與非基於 promise 的 API 整合時。
使用 Promise 進行錯誤處理
處理 Promise 中的錯誤可確保你的應用程式在出現意外情況時能正確執行。
- 你可以使用
.catch()來處理在 Promise 執行期間發生的任何錯誤或拒絕。
myPromise
.then( => .())
.catch( => .()) // Handles the rejection
.finally( => .('Promise completed')); // Runs regardless of promise resolution
- 或者,在使用
async/await時,你可以使用try/catch塊來捕獲和處理錯誤。
async function () {
try {
const = await myPromise;
.();
} catch () {
.(); // Handles any errors
} finally {
// This code is executed regardless of failure
.('performTask() completed');
}
}
();
在事件迴圈中排程任務
除了 Promise,Node.js 還提供了幾種其他機制來在事件迴圈中排程任務。
queueMicrotask()
queueMicrotask() 用於排程一個微任務,這是一個輕量級任務,在當前執行的指令碼之後、任何其他 I/O 事件或計時器之前執行。微任務包括 Promise 解決和其他優先於常規任務的非同步操作。
(() => {
.('Microtask is executed');
});
.('Synchronous task is executed');
在上面的例子中,“Microtask is executed” 將在 “Synchronous task is executed” 之後、任何 I/O 操作(如計時器)之前被列印。
process.nextTick()
process.nextTick() 用於排程一個回撥,在當前操作完成後立即執行。這對於希望確保回撥儘快執行,但仍需在當前執行上下文之後的情況非常有用。
.(() => {
.('Next tick callback');
});
.('Synchronous task executed');
setImmediate()
setImmediate() 排程一個回撥,在 Node.js 事件迴圈的檢查階段執行,該階段在輪詢階段之後執行,大多數 I/O 回撥都在輪詢階段處理。
(() => {
.('Immediate callback');
});
.('Synchronous task executed');
何時使用它們
- 對於需要在當前指令碼之後、任何 I/O 或計時器回撥之前立即執行的任務(通常用於 Promise 解決),請使用
queueMicrotask()。 - 對於應在任何 I/O 事件之前執行的任務(通常用於延遲操作或同步處理錯誤),請使用
process.nextTick()。 - 對於應在輪詢階段之後、大多數 I/O 回撥處理完畢後執行的任務,請使用
setImmediate()。
因為這些任務在當前的同步流程之外執行,所以這些回撥中的未捕獲異常不會被周圍的 try/catch 塊捕獲,並且如果管理不當(例如,透過給 Promise 附加 .catch() 或使用全域性錯誤處理器如 process.on('uncaughtException')),可能會導致應用程式崩潰。
有關事件迴圈以及各階段執行順序的更多資訊,請參閱相關文章 Node.js 事件迴圈。