2 次浏览0 条回复

一个异步操作往往同时受两类取消条件约束:自身执行超时,以及进程收到的主动停止请求。与其让业务函数分别监听多个信号,可以在调用边界用 AbortSignal.any() 合并,再把单一 signal 向下传递。

环境:Node.js 20.3+;Linux、macOS 或 Windows;仅使用内置模块;项目使用 ESM。

创建 cancel-demo.mjs

import { setTimeout as sleep } from 'node:timers/promises';

async function runJob({ signal }) {
  for (let step = 1; step <= 10; step += 1) {
    await sleep(100, undefined, { signal });
    console.log(`step ${step}`);
  }
}

const shutdown = new AbortController();
process.once('SIGINT', () => {
  shutdown.abort(new Error('shutdown requested'));
});

const signal = AbortSignal.any([
  shutdown.signal,
  AbortSignal.timeout(250),
]);

try {
  await runJob({ signal });
} catch (error) {
  if (error.name !== 'AbortError') {
    throw error;
  }

  const reason = signal.reason;
  console.error(
    'cancelled:',
    reason instanceof Error ? `${reason.name}: ${reason.message}` : reason,
  );
}

运行:

node cancel-demo.mjs

未进行人工操作时,通常先输出 step 1step 2,随后输出类似:

cancelled: TimeoutError: The operation was aborted due to timeout

也可以在超时前按 Ctrl+C,此时合并信号保留主动取消的原因,输出 cancelled: Error: shutdown requested。具体完成几个步骤受调度时序影响,不应据此写精确断言。

关键点是让 runJob 只接收外部信号,不在函数内部私自创建计时器;这样调用方可以组合超时、用户取消和服务关闭等策略。node:timers/promises、内置 fetch 等支持 signal 的 API 都可复用同一模式。捕获拒绝时检查的是操作抛出的 AbortError,而区分取消来源时读取合并信号的 reason