每位 JavaScript 開發者至少都寫過一次:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// Later…
eventBus.on('done', () => resolve(result));
Enter fullscreen mode Exit fullscreen mode
你需要在建構子外部觸發 Promise——可能是從事件監聽器、回呼或訊息處理常式中。因此你把 resolve 和 reject 洩漏到外層作用域來捕獲它們。這樣可行,但每次寫的時候都覺得不太對,因為你手動逃出一個本不該被逃出的閉包。
ES2024 為此新增了正確的工具:Promise.withResolvers。
Promise.withResolvers 會回傳什麼
與其建構一個 Promise 再逃出它的回呼,不如呼叫一個靜態方法,一次就把三個東西都交給你:
const { promise, resolve, reject } = Promise.withResolvers();
Enter fullscreen mode Exit fullscreen mode
promise 是一個普通的 Promise。resolve 和 reject 是它的解決函式,已經放在你的作用域中。不需要再逃出閉包。原本需要四行的模式,現在只需一行。
舊模式與新模式的行為完全相同——相同的微任務時機、相同的錯誤傳遞、相同的 .then/.catch 介面。差別在於現在意圖變得明確:你是有意建立一個 deferred Promise,而不是把它當作權宜之計。
事件轉 Promise 的橋樑
最清楚的使用情境是把事件型 API 包裝成 Promise 介面。之前:
function waitForOpen(socket) {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
socket.addEventListener('open', () => resolve());
socket.addEventListener('error', (e) => reject(e));
return promise;
}
Enter fullscreen mode Exit fullscreen mode
之後:
function waitForOpen(socket) {
const { promise, resolve, reject } = Promise.withResolvers();
socket.addEventListener('open', () => resolve());
socket.addEventListener('error', (e) => reject(e));
return promise;
}
Enter fullscreen mode Exit fullscreen mode
邏輯相同,但少了許多繁文縟節。函式回傳一個根據 socket 事件解析或拒絕的 Promise——這正是你從乾淨版本中看到的結果。舊版本則需要你繞過逃逸樣板才能理解。
非同步佇列與訊息傳遞
deferred Promise 反覆出現的地方是非同步佇列——消費者等待下一個項目,生產者稍後呼叫 push:
function createQueue() {
const pending = [];
let deferred = Promise.withResolvers();
return {
push(item) {
pending.push(item);
deferred.resolve();
deferred = Promise.withResolvers();
},
async *[Symbol.asyncIterator]() {
while (true) {
await deferred.promise;
while (pending.length) {
yield pending.shift();
}
}
},
};
}
const queue = createQueue();
// Consumer
(async () => {
for await (const item of queue) {
console.log('received:', item);
}
})();
// Producer (elsewhere)
queue.push('hello');
queue.push('world');
Enter fullscreen mode Exit fullscreen mode
每次呼叫 Promise.withResolvers() 都會建立一個新的閘門。消費者等待它;生產者在有東西可讀時解析它。若沒有 withResolvers,每一個閘門都需要逃逸樣板。有了它,佇列邏輯就能直接讀懂。
受控刷新:等待一批資料
另一種模式:收集項目進行批次操作,當批次觸發時一次解析所有等待者。
class Batcher {
#items = [];
#deferred = Promise.withResolvers();
add(item) {
this.#items.push(item);
return this.#deferred.promise;
}
async flush() {
const batch = this.#items.splice(0);
const { resolve } = this.#deferred;
this.#deferred = Promise.withResolvers();
const results = await this.#processBatch(batch);
resolve(results);
return results;
}
}
Enter fullscreen mode Exit fullscreen mode
呼叫者執行 await batcher.add(item),並在批次刷新時取得結果——無論是透過計時器、大小限制還是明確的使用者操作觸發。每次刷新都會用單一 Promise.withResolvers() 呼叫乾淨地重置閘門。
什麼時候不該使用它
deferred Promise 是針對特定情境的工具,而不是 Promise 建構子的一般替代方案。如果你同時控制設定與解析——fetch、fs.readFile,或任何你直接 await 的非同步 API——請直接使用 async/await 或 Promise 建構子。deferred 模式專門用於設定與解析發生在不同執行情境,無法乾淨地共用回呼的情況。
一個具體的反模式:如果你發現自己在建立 deferred 的同一個函式中同步呼叫 resolve,那你可能只是想要 new Promise(res => res(value))——也就是 Promise.resolve(value)。
TypeScript 支援
TypeScript 在 5.4 版加入了 Promise.withResolvers。回傳型別已正確標註——PromiseWithResolvers<T> 會保留型別:
const { promise, resolve, reject } = Promise.withResolvers<string>();
resolve('hello'); // ✅ string
resolve(42); // ❌ type error
Enter fullscreen mode Exit fullscreen mode
如果你使用較舊的 TS 目標,請在 tsconfig.json 的 lib 中加入 "ES2024"。
瀏覽器支援
Promise.withResolvers 已成為 Baseline 2024:Chrome 119、Firefox 121、Safari 17.4、Node.js 22。如果你針對的是現代環境,無需 polyfill 即可使用。對於較舊的目標,舊的逃逸模式仍然正確——withResolvers 只是在你原本就能手寫的東西上加了語法糖。
重點摘要
在你的程式碼庫中搜尋 let resolve 或 let reject 後面接 new Promise 的模式。每一處都是以困難方式寫成的 deferred Promise。Promise.withResolvers() 為這個模式命名,消除了逃逸樣板,並讓意圖一目了然。結果相同,但程式碼明顯更乾淨。
感謝閱讀!讓我們保持聯繫:
- ⭐ GitHub — 追蹤我並為專案按星: github.com/parsajiravand
- 📸 Instagram — 前端最佳實踐,每日更新: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email(工作與合約洽詢): [email protected]
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.