跳转到正文

AbortController 用法模式

AbortController 和 AbortSignal 是 Phasis 中完整的 WHATWG 实现。标准组合辅助方法(AbortSignal.any、AbortSignal.timeout)也已提供。本页介绍常用模式,以及取消操作如何跨越 PHP 边界传播。

取消 fetch 请求

JavaScript
const ctrl = new AbortController();
const p = fetch(url, { signal: ctrl.signal });
ctrl.abort();                     // p rejects with AbortError DOMException

默认原因为 AbortError,但你可以提供自定义原因:

JavaScript
ctrl.abort(new Error('user cancelled'));

fetch 会以你传入的任何内容作为拒绝原因。

超时

JavaScript
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });

AbortSignal.timeout(ms) 会返回一个信号,并在 ms 后自动中止。计时器通过引擎的事件循环触发,该循环也负责驱动 setTimeout / queueMicrotask(参见API:事件循环)。默认 CurlTransport 的 fetch 传输会通过 CURLOPT_PROGRESSFUNCTION 在传输过程中轮询该信号,因此一到截止时间,请求就会迅速中止。

默认情况下,循环采用虚拟时间:它会将时钟推进到下一个待处理的截止时间,而不会休眠 PHP,因此 fetch(url, { signal: AbortSignal.timeout(5000) }) 要么在截止时间前完成,要么在下一次循环刻度时中止——它绝不会让 PHP 等待。要按实时节奏运行,宿主必须根据实际时钟驱动 tickEventLoop()。

竞态:先发生的取消优先

JavaScript
const user = new AbortController();
const signal = AbortSignal.any([user.signal, AbortSignal.timeout(10_000)]);
const res = await fetch(url, { signal });
// res rejects if EITHER the user clicks cancel OR 10s elapses

跨层传播

用能够感知信号的循环包装你自己的异步工作:

JavaScript
async function fetchPaged(url, signal) {
  const out = [];
  for (let page = 1; ; page++) {
    if (signal.aborted) throw signal.reason;
    const r = await fetch(`${url}?page=${page}`, { signal });
    const data = await r.json();
    if (data.length === 0) break;
    out.push(...data);
  }
  return out;
}

fetch 会直接继承该信号;手动的 if (signal.aborted) 用于弥补请求之间的间隙,避免页面循环在此期间继续运行。

监听中止事件

JavaScript
ctrl.signal.addEventListener('abort', () => {
  releaseDatabaseLock();
  cancelDownstreamWork();
});

signal.reason 会在触发监听器之前设置。

在自定义 fetch 传输中使用信号

如果通过 setFetchTransport() 替换传输,信号会作为 JsObject 传给你的可调用对象。可通过轮询来响应它:

PHP
$engine->setFetchTransport(function (array $req, $signal) {
    $ch = curl_init($req['url']);
    // ...
    if ($signal !== null) {
        curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
            fn () => $signal->get('aborted')->value ? 1 : 0);
        curl_setopt($ch, CURLOPT_NOPROGRESS, false);
    }
    $body = curl_exec($ch);
    if (curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK) {
        throw new \Phasis\BuiltIn\Fetch\TransportException(
            'aborted', 'aborted',
        );
    }
    // ...
});

使用 kind: 'aborted' 抛出 TransportException,会使 JS 端的 fetch 以 AbortError DOMException 拒绝——效果与规范中的中止循环已经运行相同。

对于支持原生取消的后端(Guzzle 的 on_progress、Symfony 的 ResponseInterface::cancel),请接入相同的轮询并抛出逻辑。

目前尚不可行的模式

  • signal.addEventListener('abort', () => fetch(cleanupUrl)) ——监听器会同步触发;清理用的 fetch 虽然会启动,但不会在中止分发期间完成。请通过外层的 await 安排它。

setTimeout(() => ctrl.abort(), N) 可以使用(计时器系列已发布并通过 WPT 测试),但如果你只想要一个在截止时间后中止的信号,AbortSignal.timeout(N) 更简短,也更能表达意图。

另请参阅

  • Fetch 传输 — 自定义传输所需的完整请求、响应和信号接口。
  • Web API (WPT) — AbortController/AbortSignal 的 WPT 通过率。