← 中文资源

简体中文版本

与 AbortSignal 联动的延迟 Promise

暴露 Promise 的 resolve/reject 控制,并在绑定信号中止时自动以 AbortError 拒绝。

阅读英文原文

功能说明

该示例实现 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());