登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

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 或响应体读取阶段再根据该信号停止工作。

AbortController、Request.signal、fetch 请求和 AbortError 之间的取消关系结构说明图
图1:结构说明图,展示 controller.signal 如何关联 Request 与正在进行的 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,但代码仍要根据错误名称或业务状态区分原因。

Fetch AbortSignal 的用户取消、超时取消、错误分流和生命周期边界说明图
图2:边界说明图,展示取消来源和错误处理分流,不是运行截图或线上监控证据。
// 用户取消和超时共享一个请求入口,但保留各自的生命周期
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。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>