AbortController 用法模式
AbortController 和 AbortSignal 是 Phasis 中完整的 WHATWG 实现。标准组合辅助方法(AbortSignal.any、AbortSignal.timeout)也已提供。本页介绍常用模式,以及取消操作如何跨越 PHP 边界传播。
取消 fetch 请求
const ctrl = new AbortController();
const p = fetch(url, { signal: ctrl.signal });
ctrl.abort(); // p rejects with AbortError DOMException
默认原因为 AbortError,但你可以提供自定义原因:
ctrl.abort(new Error('user cancelled'));
fetch 会以你传入的任何内容作为拒绝原因。
超时
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()。
竞态:先发生的取消优先
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
跨层传播
用能够感知信号的循环包装你自己的异步工作:
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) 用于弥补请求之间的间隙,避免页面循环在此期间继续运行。
监听中止事件
ctrl.signal.addEventListener('abort', () => {
releaseDatabaseLock();
cancelDownstreamWork();
});
signal.reason 会在触发监听器之前设置。
在自定义 fetch 传输中使用信号
如果通过 setFetchTransport() 替换传输,信号会作为 JsObject 传给你的可调用对象。可通过轮询来响应它:
$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 通过率。