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 內註冊相同的監聽與清理。
參考資料: