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

JavaScript fetch 怎样设置超时并可取消

来源:17golang原创

时间:2026-10-23 11:10:54 100浏览 收藏

JavaScript fetch 设置超时并允许手动取消,可以把 AbortSignal.timeout 产生的超时信号与 AbortController 的用户信号通过 AbortSignal.any 合并,再传给 fetch 的 signal。封装 JSON 请求时,应把 response.json() 也放进这次操作范围;只等到响应头就结束计时,会漏掉缓慢的响应体。

这里把它看成“调用方拥有取消权”的请求边界:上层决定何时放弃,下层负责把信号传到网络读取,并处理自己的资源。示例针对浏览器,接口地址为占位路径,本文没有运行代码或构造实际网络结果。

把请求取消作为调用契约

搜索框切换查询、页面离开、用户点取消,都可能让请求失去价值。只设置一个全局超时不够:不同调用有不同的预算,也不应互相取消。一个独立操作配一个控制器,下层接收信号而不是直接控制整个页面。

AbortSignal 是单次信号,中止后不能恢复。下一次请求应创建新的控制器。复用已取消信号会让新请求立即失败;有意共享同一信号则代表多个请求属于同一个取消域。接口语义依据 WHATWG DOM 标准。

用组合信号实现超时与手动取消

async function fetchJson(url, { signal, timeoutMs = 6000 } = {}) {
  // 约束参数范围,避免无意间把秒当成毫秒
  if (!Number.isInteger(timeoutMs) || timeoutMs  2147483647) {
    throw new RangeError("timeoutMs 必须是有效的非负毫秒整数");
  }
  const deadline = AbortSignal.timeout(timeoutMs);
  // 外部取消与超时共用一个信号,最先中止的原因保留下来
  const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
  try {
    const response = await fetch(url, { signal: combined });
    if (!response.ok) {
      // 不读取错误响应体,主动取消未消费的流;清理失败不覆盖 HTTP 错误
      if (response.body) await response.body.cancel().catch(() => {});
      throw new Error(`HTTP ${response.status}`);
    }
    // await 放在函数内,整个读取操作都受同一个取消信号约束
    return await response.json();
  } catch (error) {
    // 仅当拒绝值就是该信号原因时,按取消归类,避免掩盖独立错误
    if (combined.aborted && error === combined.reason) throw combined.reason;
    throw error;
  }
}

这里的 timeoutMs 使用毫秒,6000 只是示例预算,不是所有接口的推荐值。运行前应确认目标环境提供 timeout 和 any 两个静态方法,不能只检测 AbortController 就假设全部支持。传入的 signal 应是有效 AbortSignal;不接受伪造的普通对象。

any 的组合结果在任一来源中止时中止,并携带触发来源的原因。输入已经中止时,组合结果也立即中止;多个输入事先都已中止时,按输入顺序采用第一个。没有传外部信号时,无需额外合并。

用户控制器、超时信号与组合信号的静态归属关系
用户控制器、超时信号与组合信号的静态归属关系(原创静态结构说明图,不是运行证据)。
// 每次操作新建控制器,把同一信号交给这一操作的请求
const userCancel = new AbortController();
const task = fetchJson("/api/catalog", {
  signal: userCancel.signal,
  timeoutMs: 6000,
});
// 立即接住异步失败;实际页面可改为展示相应提示
task.catch((error) => {
  if (error?.name === "TimeoutError") console.info("请求超时");
  else if (error?.name === "AbortError") console.info("已取消");
  else console.error("请求失败", error);
});
// 将下面这一行放进真实取消事件回调;不需要取消时不要执行
// userCancel.abort(new DOMException("用户取消查询", "AbortError"));

示例约定用户取消使用 AbortError、超时使用 TimeoutError。调用方如果自定义 abort 的 reason,它可以是任意 JavaScript 值,就不能继续假定每个拒绝值都有 name 属性。业务封装应明确自己的错误契约,而不是把所有异常都提示成网络中断。

让超时覆盖响应体读取

fetch 的 Promise 可以在响应体尚未读完时兑现。随后 json 或 text 还要消费流,期间取消仍可能导致读取失败。示例把 await response.json() 留在封装内,因此调用方拿到的是解析后的数据,不是一个尚待消费的 Response。相关网络与流取消语义见 WHATWG Fetch 标准。

HTTP 404、500 不会仅因为状态码就使 fetch 拒绝,因此代码显式检查 response.ok。JSON 格式错误、网络异常和中止也不是一类问题。错误发生后不能只看某个来源信号后来是否中止就改写原异常;示例在需要核对时把拒绝值与组合信号 reason 对齐。

这个边界是网络读取预算,不是抢占式 CPU 限时器。JavaScript 同步解析或业务计算正在占用线程时,取消信号不能强制打断它。原生 timeout 基于活跃时间,页面或 worker 暂停可能影响到期时刻,不能把它当成严格的墙钟服务端截止期限。

旧环境用定时器和监听器桥接

如果环境有基本取消能力但缺少两个静态方法,可以用本地控制器连接外部信号和定时器。这个兼容版本按同一参数约定工作,重点是成功和失败都清理资源,而不是仅在超时后清理。

async function fetchJsonCompat(url, { signal, timeoutMs = 6000 } = {}) {
  // 仅替代 timeout/any;仍要求支持 fetch、AbortController 和 signal.reason
  if (!Number.isInteger(timeoutMs) || timeoutMs  2147483647) {
    throw new RangeError("timeoutMs 超出定时器安全范围");
  }
  const local = new AbortController();
  const forward = () => local.abort(signal.reason);
  let timer;
  try {
    // 已取消的输入不能等待事件再次触发
    if (signal) {
      if (signal.aborted) forward();
      else signal.addEventListener("abort", forward, { once: true });
    }
    if (!local.signal.aborted) {
      timer = setTimeout(() => {
        local.abort(new DOMException("请求超时", "TimeoutError"));
      }, timeoutMs);
    }
    const response = await fetch(url, { signal: local.signal });
    if (!response.ok) {
      if (response.body) await response.body.cancel().catch(() => {});
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } finally {
    // 成功、HTTP 错误、网络错误及取消都走同一清理出口
    clearTimeout(timer);
    signal?.removeEventListener("abort", forward);
  }
}
兼容实现中控制器、定时器、监听器与 JSON 读取的资源归属
兼容实现中控制器、定时器、监听器与 JSON 读取的资源归属(原创静态结构说明图,不是运行证据)。

finally 包围响应体读取,意味着无论读取成功还是中途拒绝,定时器与外部监听都会释放。只有 once 并不够:成功请求从未触发 abort,监听器就不会自动因 once 消失。另一方面,原生 timeout 没有与 clearTimeout 对应的公开取消方法,any 也不会替你中止其他来源。

这不是 timeout 的完全语义 polyfill:普通定时器受到页面调度影响,旧引擎还可能缺少自定义中止原因支持。若连基础 AbortController 都没有,应采用经过项目兼容评估的实现或调整支持范围,不能用一个名字相同的空函数伪装已经取消网络。

不要把取消当成回滚

Promise.race 只能让调用方先收到某个结果;如果没有给 fetch 传入真正的取消信号,败下来的请求仍可能继续。即便正确调用 abort,服务端也可能已经收到请求并完成写入,客户端取消不提供事务回滚承诺。

查询取消后通常可以忽略旧结果;支付、创建订单等写操作则需要服务端幂等设计、状态查询和明确业务协议,不能因客户端超时就自动认为失败并无条件重试。相邻请求还应通过请求标识避免旧响应覆盖新状态,取消不是所有界面竞态的替代品。

需求对应机制不保证什么
用户放弃请求控制器与外部信号服务端撤回已提交操作
限定网络等待超时信号或可清理定时器同步代码精确限时
组合多种取消any 或事件桥接中止后能够恢复信号

接入时核对的边界和疑问

接入时明确:超时单位、浏览器支持范围、响应体由哪层消费、用户取消原因、HTTP 错误格式、组件离开时谁中止,以及兼容实现谁清理监听器。若封装返回 Response 而不是 JSON,响应体消费和清理责任必须一并转交,不能照搬本文函数的生命周期。

能给 fetch 直接加 timeout 参数吗?标准 RequestInit 没有一个通用 timeout 字段;应传 signal。第三方客户端的同名选项属于其自身 API,不能直接套到原生 fetch。

取消后下一次请求为什么立即报错?很可能复用了已经中止的信号。每次独立操作创建新控制器,只在需要成组取消时共享。

超时和手动取消都发生时怎样分类?以真正触发组合中止的 reason 为准,并遵守调用方约定的错误形状;不要在 catch 中重新比较当前所有来源的 aborted 状态推测先后。

把取消权、读取范围和清理责任写进请求函数的契约,比给每个调用临时套一个延时 Promise 更容易维护。本文代码与图示均是原创说明示例,具体效果仍由实际浏览器、接口及业务协议决定。

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