📋 目錄
這是 TC39 新語法深度解析系列的第四篇。當你需要在一個地方建立 Promise,在另一個地方 resolve/reject,過去的 workaround 總是冗長而且容易寫錯。
Promise.withResolvers()把這個模式變成一行。
Deferred Promise 問題:需要把 resolve 和 reject 拿出來
有時候你需要在一個地方建立 Promise,但希望 resolve 和 reject 可以在另一個地方呼叫——例如,當你需要把 Promise 傳給外部系統,但由你的程式邏輯決定什麼時候完成。
// 過去的做法(尷尬)
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// 問題:resolve 和 reject 是 let 變數,可能被意外覆寫
// 而且 Promise executor 立即執行——有時浪費
// 使用時:
fetchSomething()
.then(data => resolve(data))
.catch(err => reject(err));
// 等 promise 在某處被使用
Promise.withResolvers():一行解決
// 一行:返回 { promise, resolve, reject }
const { promise, resolve, reject } = Promise.withResolvers();
// 使用時:
fetchSomething()
.then(data => resolve(data))
.catch(err => reject(err));
// 使用 promise:
return promise; // 只返回 promise,不暴露 resolve/reject
這個模式在封裝(encapsulation)上很有價值——你返回一個 Promise 給外部,但把控制權留在一個可控的地方。
實際應用場景
與 AbortController 整合:可取消的 fetch
function fetchWithTimeout(url, options = {}) {
const { promise, resolve, reject } = Promise.withResolvers();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch(url, { signal: controller.signal })
.then(res => {
clearTimeout(timeoutId);
resolve(res);
})
.catch(err => {
clearTimeout(timeoutId);
reject(err);
});
return {
promise,
abort: () => controller.abort()
};
}
// 使用
const { promise, abort } = fetchWithTimeout('/api/data');
promise.then(data => console.log(data));
// 取消
setTimeout(abort, 100); // 5秒內取消
事件驅動的 bridge
// 將 DOM 事件橋接到 Promise
function once(element, eventName) {
const { promise, resolve } = Promise.withResolvers();
if (eventName === 'error') {
element.addEventListener('error', resolve, { once: true });
} else {
element.addEventListener(eventName, resolve, { once: true });
}
return promise;
}
// 使用:等待圖片載入
const img = new Image();
img.src = '/photo.jpg';
document.body.appendChild(img);
const loaded = once(img, 'load');
const error = once(img, 'error');
Promise.race([loaded, error])
.then(() => console.log('圖片載入成功'))
.catch(() => console.log('圖片載入失敗'));
測試工具:等待某個函式被呼叫
function mockOnce(fn) {
const { promise, resolve } = Promise.withResolvers();
const original = fn;
fn = (...args) => {
fn = original; // 恢復原函式
resolve(args); // 呼叫時 resolve
};
return { promise, mock: fn };
}
// 在測試中:
const { promise, mock } = mockOnce(console.log);
user.updateName('Alice');
promise.then(([name]) => {
console.log(name); // 'Alice'
});
與傳統模式的比較
| 做法 | 語法 | 問題 |
|---|---|---|
let resolve, reject; new Promise(...) | 多行、let 變數 | 可被意外覆寫 |
Promise.withResolvers() | 一行 | 沒有(推薦) |
瀏覽器支援
ES2024——所有主流瀏覽器與 Node.js 22+ 都已原生支援:
// Node.js 確認
node -e "const {promise} = Promise.withResolvers(); console.log(promise instanceof Promise)"
// true
總結
Promise.withResolvers() 是個小語法,但它是那種「讓你回頭看舊代碼,納悶以前是怎麼忍受」的功能之一。適合用於:
- 需要把 Promise 控制權(resolve/reject)封裝在內部
- 需要與 AbortController 或其他取消機制整合
- 測試時需要等待特定函式被呼叫
這是 TC39 新語法深度解析系列的第四篇。下一篇:Iterator Helpers — 迭代器終於有了 map、filter、take。
延伸閱讀
- TypeScript 6.0 RC 升級指南 — 支援 Promise.withResolvers
本文基於 ES2024 Promise.withResolvers(Stage 4)整理。