Fetch Request.signal 怎样取消已经发出的请求
来源:17golang原创
时间:2026-10-27 01:00:49 300浏览 收藏
在搜索框、列表筛选或页面切换中,旧的 Fetch 请求经常已经发出,但它的结果已经不值得渲染。此时不能把 Request.signal 当成“取消按钮”:它只是与请求关联的只读 AbortSignal。真正的取消动作由创建它的 AbortController 执行。
官方参考:https://developer.mozilla.org/en-US/docs/Web/API/Request/signal
正确做法是:创建一个新的AbortController,在构造Request或调用fetch时传入controller.signal,请求已经开始后调用controller.abort()。如果一个已经发出的请求从未绑定 signal,就不能通过事后读取或修改request.signal把它补成可取消请求。
先把取消链路接对
Request.signal 是只读属性,返回的是当前请求关联的信号。它能让代码观察 aborted、reason 和 abort 事件,但没有 abort() 方法。取消链路应当从控制器开始:控制器持有取消权,signal 被传给请求,fetch 或响应体读取阶段再根据该信号停止工作。

请求发出后怎样取消
下面的例子先构造带 signal 的 Request,再立即启动 fetch。按钮事件、路由切换或新的搜索词到来时,都可以调用同一个 controller 的 abort()。请求是否已经真正离开浏览器并不改变调用方式,取消动作只需要针对仍在进行的异步操作。
// 每次请求创建独立控制器,避免不同搜索任务互相取消
const controller = new AbortController();
const request = new Request("/api/search?q=fetch", {
// Request.signal 会关联到这个控制器的 signal
signal: controller.signal,
});
async function loadResult() {
try {
// fetch 已启动后,调用 controller.abort() 仍可中止进行中的请求
const response = await fetch(request);
const data = await response.json();
return data;
} catch (error) {
// 主动取消不是普通网络故障,避免把它记成接口错误
if (request.signal.aborted) {
console.info("请求已取消", request.signal.reason);
return null;
}
throw error;
}
}
const resultPromise = loadResult();
// 这里模拟用户切换筛选条件后取消旧请求
controller.abort("新的筛选条件已经到来");
await resultPromise;
调用 abort() 后,fetch() 的 Promise 通常会以名为 AbortError 的异常结束;如果传入了自定义 reason,应保留它用于日志或界面决策。这里通过 request.signal.aborted 判断取消状态,而不是把所有 catch 都当成“请求失败”。
不要漏掉响应体读取阶段
“请求已发出”不等于“所有网络工作都结束”。即使 fetch 已经拿到 Response,如果响应体还没有读完,取消也可能发生在 response.json()、response.text() 或流读取阶段。因此,实际项目要让同一个 signal 覆盖从请求到响应体消费的完整生命周期。
// 把请求和响应体读取放进同一个 try,覆盖完整生命周期
async function readJson(url, signal) {
try {
// signal 同时控制等待响应和后续 body 消费
const response = await fetch(new Request(url, { signal }));
if (!response.ok) {
// HTTP 4xx/5xx 不是 AbortError,保留状态码给上层处理
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
// 主动取消时返回可识别状态,其他错误继续抛出
if (signal.aborted) {
return { canceled: true, reason: signal.reason };
}
throw error;
}
}
const controller = new AbortController();
const promise = readJson("/api/profile", controller.signal);
// body 尚未读完时也可以结束这次消费
controller.abort("页面已经离开");
await promise;
这个边界很重要:如果代码只在 await fetch() 外层处理异常,却把 response.json() 放到另一个没有取消判断的函数里,用户看到的可能仍是“加载失败”,而不是一次预期内的主动取消。
一个 signal 只服务一段生命周期
AbortSignal 一旦进入 aborted 状态,就不能恢复。新搜索词到来时,应创建新的 controller,并让旧 controller 只负责旧请求。不要把一个全局 controller 永久放在模块级变量里,然后把已经取消的 signal 继续传给后续 fetch;后续请求会立即走取消分支。
// 每个任务都返回自己的取消函数,避免共享一个已结束的 signal
function startSearch(keyword) {
const controller = new AbortController();
const task = fetch(`/api/search?q=${encodeURIComponent(keyword)}`, {
// 当前搜索任务独占这个 signal
signal: controller.signal,
}).then((response) => response.json());
return {
task,
cancel() {
// 只取消当前任务,不影响下一个关键词的请求
controller.abort("搜索词已替换");
},
};
}
let current = startSearch("第一组");
current.cancel();
// 新任务会创建新 controller,不能复用上一个 current 的 signal
current = startSearch("第二组");
在搜索建议、分页切换、组件卸载等场景,这种“一个任务一个 controller”的边界比把取消权放进共享状态更容易审计,也能减少误取消仍在使用的请求。
把用户取消和超时放在同一条处理链
生产请求通常有两种取消来源:用户主动离开,以及请求超过允许等待时间。可以用 AbortSignal.timeout() 生成超时信号,再用 AbortSignal.any() 与用户 controller 合并。这样 fetch 只接收一个 signal,但代码仍要根据错误名称或业务状态区分原因。

// 用户取消和超时共享一个请求入口,但保留各自的生命周期
async function fetchWithCancel(url, userSignal) {
const timeoutSignal = AbortSignal.timeout(5000);
// 任意一个 signal 结束,请求就进入取消流程
const signal = AbortSignal.any([userSignal, timeoutSignal]);
try {
const response = await fetch(url, { signal });
return await response.json();
} catch (error) {
// 超时和用户取消都不是普通网络失败,记录不同业务原因
if (error?.name === "TimeoutError") {
return { canceled: true, reason: "timeout" };
}
if (userSignal.aborted || signal.aborted) {
return { canceled: true, reason: "user" };
}
throw error;
}
}
const userController = new AbortController();
const task = fetchWithCancel("/api/report", userController.signal);
// 页面卸载或用户点击停止时调用
userController.abort("用户主动停止");
await task;
组合 signal 的好处是取消入口统一;代价是组合后的 signal 只告诉你“某个来源已结束”。如果业务必须精确区分每一种原因,就要在调用前后分别记录用户 controller 和超时策略,而不是只依赖组合 signal 的最终状态。
发布前检查这五个边界
- 绑定时机:signal 必须在创建 Request 或调用 fetch 时传入;没有绑定 signal 的既有请求,不能靠读取
Request.signal事后补绑。 - 控制权:调用
controller.abort(),不要尝试给request.signal增加取消方法。 - 错误分类:主动取消、超时、HTTP 错误和普通网络异常分别记录,避免取消比例污染故障指标。
- 生命周期:一个 controller 对应一个请求任务;页面切换或新查询到来时释放旧任务并创建新的 signal。
- 响应体:把 JSON、文本或流的读取也放在同一条取消处理链里,不能只保护 fetch 建连阶段。
| 遇到的问题 | 正确判断 |
|---|---|
| 已经有一个 Request,想临时取消 | 只有它创建时绑定了可达的 signal,才能通过对应 controller 取消 |
| 调用 abort 后 catch 触发 | 先看 signal.aborted 或错误名称,再决定是否记录为系统失败 |
| 新请求一发出就被取消 | 检查是否复用了已经 aborted 的 signal |
| fetch 成功但 json 读取失败 | 检查响应体读取期间是否被 abort |
因此,Request.signal 的作用是把请求接入取消生命周期,而不是提供一个可事后安装的停止按钮。只要在请求创建时绑定 signal、在请求和 body 读取期间保持同一条处理链,并把主动取消从故障统计中分离出来,就能稳定处理“已经发出的请求”。
相关问题
Request.signal 可以直接调用 abort() 吗?
不可以。它是只读的 AbortSignal,取消权在创建它的 AbortController 上,应调用对应的 controller.abort()。
fetch 已经返回 Response 后还能取消吗?
如果响应体还在读取,仍可能被 signal 中止;因此要把 response.json() 或流消费放进同一个异常处理范围。
AbortController 取消后能再次发请求吗?
不能复用已经 aborted 的 signal 作为新任务的控制信号。重新创建一个 AbortController,再把新的 signal 传给下一次 fetch。
-
260 收藏
-
118 收藏
-
410 收藏
-
202 收藏
-
209 收藏
-
437 收藏
-
211 收藏
-
446 收藏
-
498 收藏
-
253 收藏
-
221 收藏
-
157 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习