681 字
3 分鐘

Promise.withResolvers 怎麼用?把一次性事件轉成可取消的等待

本文目錄

如果操作由一次 ready 事件完成,呼叫端卻需要用 await 等待,可以用 Promise.withResolvers() 同時取得 promise、resolve 和 reject。事件與取消訊號各有自己的入口,兩條路徑都要移除監聽器。Promise 只會採用第一次決定的結果,但後來的事件仍能觸發留下來的 callback。

先註冊監聽,再啟動會發事件的操作#

下面的 helper 使用標準 EventTarget,並要求呼叫端傳入 AbortSignal。成功時交還原事件,取消時以 signal.reason 拒絕。如果 signal 已中止,直接回傳 rejected Promise,省去註冊後再清理的流程。範例可獨立存成 wait-for-event.mjs,用 Node.js v24.13.0 執行。

import assert from 'node:assert/strict';
import { getEventListeners } from 'node:events';
function waitForEvent(target, type, signal) {
if (signal.aborted) return Promise.reject(signal.reason);
const { promise, resolve, reject } = Promise.withResolvers();
function cleanup() {
target.removeEventListener(type, onEvent);
signal.removeEventListener('abort', onAbort);
}
function onEvent(event) {
cleanup();
resolve(event);
}
function onAbort() {
cleanup();
reject(signal.reason);
}
target.addEventListener(type, onEvent);
signal.addEventListener('abort', onAbort);
return promise;
}
const target = new EventTarget();
const completed = new AbortController();
const waiting = waitForEvent(target, 'ready', completed.signal);
const first = new Event('ready');
target.dispatchEvent(first);
assert.equal(getEventListeners(target, 'ready').length, 0);
assert.equal(getEventListeners(completed.signal, 'abort').length, 0);
target.dispatchEvent(new Event('ready'));
completed.abort();
assert.equal(await waiting, first);
const cancelled = new AbortController();
const reason = new Error('畫面已關閉');
const pending = waitForEvent(target, 'ready', cancelled.signal);
cancelled.abort(reason);
assert.equal(getEventListeners(target, 'ready').length, 0);
assert.equal(getEventListeners(cancelled.signal, 'abort').length, 0);
target.dispatchEvent(new Event('ready'));
await assert.rejects(pending, error => error === reason);
await assert.rejects(
waitForEvent(target, 'ready', cancelled.signal),
error => error === reason,
);
assert.equal(getEventListeners(target, 'ready').length, 0);
assert.equal(getEventListeners(cancelled.signal, 'abort').length, 0);
console.log('事件、取消及已中止訊號檢查通過');

在可能同步發出事件的程式裡,要先呼叫 waitForEvent(...) 建立等待,再啟動操作。若操作也會發出獨立的失敗事件,應把它接入同一套清理流程,並交由 reject 傳回錯誤。只給成功事件設 { once: true },取消時仍須移除成功監聽,成功時也要移除取消監聽。站內另有 用 AbortController 清理多個事件監聽 的 DOM 寫法。

每次等待都建立自己的 Promise#

在同一個元件重複啟動時,每次呼叫都建立新 Promise,舊操作由它的 controller 結束。Promise 若要等的是另一個已存在的 Promise,直接 await 或回傳它即可。這個模式適用於完成時機由事件、callback 或工作佇列控制的情況。withResolvers 只是把建構 Promise 時的三個值放在同一個物件,取消仍由事件來源和程式的清理規則負責。 MDN Promise.withResolvers

需要設定最久等待時間時,可傳入 AbortSignal.timeout(...)。這會結束上例的等待,原本啟動的操作若也得停止,需要把 signal 交給支援取消的 API,或在取消時另外處理。舊執行環境則先用 typeof Promise.withResolvers === 'function' 檢查支援度,也可以使用 new Promise(executor) 並在 executor 內註冊相同的監聽與清理。

參考資料:

MDN:Promise.withResolvers()

MDN:AbortSignal

MDN:EventTarget.removeEventListener()

Promise.withResolvers 怎麼用?把一次性事件轉成可取消的等待
https://laplusda.com/posts/javascript-promise-withresolvers-event-cleanup/
作者
Zero
發佈於
2026-11-08
許可協議
CC BY-NC-SA 4.0