功能说明
该示例实现 Deferred 模式:把 Promise 的 resolve 与 reject 暴露到外部,让回调、事件或定时器可以在稍后完成它。同时绑定 AbortSignal,用户取消操作时 Promise 会自动以 AbortError 拒绝。适合把回调式 API 包装成 Promise、等待多个异步来源中的任意一个,或在组件卸载时取消挂起任务。
关键代码
createDeferred 在 Promise 执行器中保存 resolve 与 reject,并用 settled 标志控制只能结算一次。markSettled 通过 then 的两个分支同步状态。onAbort 在未结算时用 DOMException 构造 AbortError 并拒绝。若信号已经中止则立即触发,否则注册一次性 abort 监听,并在 Promise 结算后通过 detach 移除监听,避免内存泄漏。
使用注意
被中止的 Promise 若没有调用方 catch,会产生未处理拒绝,调用方必须处理。resolve 与 reject 在已结算后调用会被忽略,不会抛错。signal 必须是真正的 AbortSignal 实例,跨 realm 传入可能无法通过 instanceof。超时场景可以用 AbortSignal.timeout 组合,但多个信号需要 AbortSignal.any。不要在 Deferred 中存放无法取消的底层资源。
验证方式
创建绑定 AbortController 的 Deferred,调用 abort 后断言 Promise 以 AbortError 拒绝且 settled 为 true。测试手动 resolve 后再次 resolve 与 reject 都不改变结果。传入已中止的信号断言立即拒绝。再验证非 AbortSignal 参数抛 TypeError,以及正常 resolve 时会移除事件监听。
代码示例
export interface Deferred<Value> {
readonly promise: Promise<Value>;
readonly settled: () => boolean;
resolve(value: Value | PromiseLike<Value>): void;
reject(reason?: unknown): void;
}
export function createDeferred<Value>(signal?: AbortSignal): Deferred<Value> {
if (signal !== undefined && !(signal instanceof AbortSignal)) {
throw new TypeError("signal must be an AbortSignal");
}
let settle: ((value: Value | PromiseLike<Value>) => void) | undefined;
let fail: ((reason?: unknown) => void) | undefined;
let settled = false;
const promise = new Promise<Value>((resolve, reject) => {
settle = resolve;
fail = reject;
});
const markSettled = (): void => {
settled = true;
};
void promise.then(markSettled, markSettled);
const onAbort = (): void => {
if (settled || fail === undefined) return;
settled = true;
fail(new DOMException("The operation was aborted", "AbortError"));
};
if (signal?.aborted === true) {
onAbort();
} else if (signal !== undefined) {
signal.addEventListener("abort", onAbort, { once: true });
const detach = (): void => signal.removeEventListener("abort", onAbort);
void promise.then(detach, detach);
}
return {
promise,
settled: () => settled,
resolve(value) {
if (settled || settle === undefined) return;
settled = true;
settle(value);
},
reject(reason) {
if (settled || fail === undefined) return;
settled = true;
fail(reason);
},
};
}
const controller = new AbortController();
const aborted = createDeferred<number>(controller.signal);
aborted.promise.catch((error: unknown) => {
console.log(error instanceof DOMException ? error.name : "unknown");
});
controller.abort();
console.log(aborted.settled());
const manual = createDeferred<string>();
manual.promise.then((value) => console.log(value));
manual.resolve("done");
manual.resolve("ignored");
console.log(manual.settled());