当前位置:首页 > 文章列表 > 文章 > 前端 > JavaScript AbortSignal.any 怎么组合多个取消来源

JavaScript AbortSignal.any 怎么组合多个取消来源

来源:17golang原创 2026-10-04 11:08:01 0浏览 收藏

当一次请求可能被“用户点击取消”“页面离开”“组件销毁”或“超时”中的任意条件终止时,不必手工把多个 abort 事件转发到同一个控制器。直接把各来源的 AbortSignal 交给 AbortSignal.any(),得到一个组合信号,再传给 fetch 或其他支持 signal 的 API。

组合信号在任一输入信号中止时中止,reason 使用最先中止的那个来源。如果传入时已有信号中止,返回值会立即处于中止状态;组合信号不会反向中止其他输入控制器。

MDN 参考地址:https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static

为什么需要组合取消来源

单个 AbortController 只表达一个控制点。实际页面通常同时存在几类终止条件:

  • 用户主动点击“取消上传”或“停止搜索”。
  • 路由切换、弹窗关闭或组件卸载,当前结果已经没有消费者。
  • 业务设置最大等待时间,避免请求长期占用连接和 UI 状态。
  • 上层调用者已经有一个 signal,希望把取消权继续向下传递。

如果每个业务函数都手写事件转发,很容易漏掉监听器清理、丢失真实原因,或在请求完成后还保留对页面对象的引用。AbortSignal.any() 把“任一来源都能终止”变成明确的数据关系,业务 API 只接收一个 signal。

最小写法:三个来源合成一个 signal

下面把用户按钮、页面生命周期和 10 秒超时合并。任何来源先触发,fetch 都会停止。

AbortSignal.any 多取消来源关系

图1:多个取消来源汇入组合信号,首个中止原因决定 combinedSignal.reason。

const userController = new AbortController();
const pageController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10_000);

// 任一输入中止,组合信号都会中止
const combinedSignal = AbortSignal.any([
  userController.signal,
  pageController.signal,
  timeoutSignal,
]);

cancelButton.addEventListener("click", () => {
  // 保存明确原因,便于 catch 和日志区分来源
  userController.abort(
    new DOMException("用户主动取消", "AbortError"),
  );
}, { once: true });

async function loadDetail() {
  try {
    const response = await fetch("/api/detail/42", {
      signal: combinedSignal, // fetch 只需接收一个组合信号
    });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    if (error.name === "TimeoutError") {
      console.warn("请求超时"); // timeout() 提供超时原因
      return null;
    }
    if (error.name === "AbortError") {
      console.info("请求已取消", combinedSignal.reason);
      return null;
    }
    throw error; // 网络或业务错误继续交给上层
  }
}

页面离开时调用 pageController.abort() 即可。如果项目使用 React、Vue 或其他组件框架,应把它放在对应的清理回调中。关键不是框架语法,而是“页面生命周期拥有自己的控制器”,不要让用户按钮和页面销毁共用同一个来源。

首个中止原因如何决定 reason

AbortSignal.any(iterable) 接收数组等可迭代对象,返回新的 AbortSignal。原因判定有两种情况:

输入状态组合信号结果reason 来源
调用时都未中止保持可用,等待第一个来源中止运行过程中最先中止的来源
调用时已有一个或多个中止返回时已经中止输入迭代顺序中第一个已中止信号

因此,把组合信号传入操作前可以先调用 throwIfAborted(),避免为一个已经失效的操作继续创建昂贵资源。

function startExpensiveTask(signals) {
  const signal = AbortSignal.any(signals);

  // 如果某个输入早已中止,这里立即抛出对应 reason
  signal.throwIfAborted();

  return runWorkerJob({ signal });
}

同一个 AbortSignal 是一次性的:一旦中止,就不会恢复。不要把页面级已中止信号保存到全局并复用于下一次请求,否则下一次操作会在创建时立即失败。正确做法是“一次操作创建一组新的来源控制器和一个新的组合信号”。

给每个来源保留可审计原因

只看 AbortError 往往无法区分用户取消和页面离开。可以让每个控制器传入带来源字段的 Error。由于组合信号保留首个来源的 reason,上层就能决定是否提示用户、静默结束或记录性能指标。

class CancellationError extends Error {
  constructor(source, message) {
    super(message);
    this.name = "AbortError";
    this.source = source; // 记录取消来源,而不是解析错误文本
  }
}

function cancelForRouteChange(controller) {
  controller.abort(
    new CancellationError("route-change", "页面已离开"),
  );
}

function cancelByUser(controller) {
  controller.abort(
    new CancellationError("user", "用户主动取消"),
  );
}

不要在日志中把所有中止都记成错误。用户主动取消和页面离开通常是正常控制流,可以记录为 info 或指标;超时可能表示性能问题;网络错误和 HTTP 错误则应走原有失败处理。分类时优先读取捕获到的 error 或组合信号的 reason,不要依赖本地布尔变量猜测谁先发生。

封装一个可复用的请求函数

业务函数可以接收上层 signal,再添加自己的超时来源。这样页面负责生命周期,函数负责最大执行时间,双方职责清晰。

async function fetchJson(url, {
  signal: externalSignal,
  timeoutMs = 8_000,
} = {}) {
  const sources = [AbortSignal.timeout(timeoutMs)];

  // 只有调用者传入 signal 时才加入组合
  if (externalSignal) {
    sources.unshift(externalSignal);
  }

  const signal = AbortSignal.any(sources);
  signal.throwIfAborted(); // 不启动已经失效的请求

  const response = await fetch(url, { signal });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

const viewController = new AbortController();

try {
  const data = await fetchJson("/api/report", {
    signal: viewController.signal,
    timeoutMs: 12_000,
  });
  renderReport(data); // 只有成功结果才更新页面
} catch (error) {
  if (error.name === "TimeoutError") {
    showRetryMessage(); // 超时允许用户重试
  } else if (error.name !== "AbortError") {
    reportUnexpectedError(error);
  }
}

这个封装没有试图中止调用者的控制器。下层函数只能观察外部 signal,不能替上层恢复或重置它。超时触发后,外部控制器仍保持原状态;反过来,外部 signal 先中止时,超时信号也不会被取消,但已没有进行中的 fetch 需要它处理。

四个容易误判的边界

AbortSignal.any 生命周期与风险边界

图2:AbortSignal.any 的生命周期边界,组合中止不会反向取消其他来源。

  1. 组合信号没有控制器。AbortSignal.any() 返回 signal,不提供“主动中止组合信号”的方法。主动取消必须来自输入控制器之一。
  2. 传播是单向的。输入来源能中止组合信号,但组合信号中止不会让其他输入 signal 也中止,也不会取消已经创建的 timeout。
  3. 首因胜出。多个来源几乎同时触发时,只有第一个原因进入 combinedSignal.reason。后续来源不能覆盖它。
  4. 没有手动解绑输入的 API。平台内部会管理组合关系,但业务自己添加到组合信号上的 abort 监听器仍应在操作完成时移除。

自定义监听器要在完成后清理

fetch 会自己处理 signal。若业务还给组合信号添加了进度停止、资源释放等监听器,可以用一个专门的清理控制器管理监听器生命周期。

async function runWithCleanup(signal) {
  const listenerScope = new AbortController();

  signal.addEventListener("abort", () => {
    stopProgressAnimation(); // 中止时立即停止额外资源
  }, {
    once: true,
    signal: listenerScope.signal,
  });

  try {
    signal.throwIfAborted();
    return await fetch("/api/slow-task", { signal });
  } finally {
    // 正常完成时也移除监听器,避免长期引用页面对象
    listenerScope.abort();
    stopProgressAnimation();
  }
}

{ once: true } 只保证事件触发后移除监听器;如果操作正常完成且从未中止,它不会自动触发。因此仍需要在 finally 中清理。对于高频搜索、滚动加载和反复打开关闭的弹窗,这一点尤其重要。

兼容性与降级

MDN 将 AbortSignal.any() 标记为自 2024 年 3 月起跨主流浏览器广泛可用,并支持 Web Worker。若项目仍覆盖旧版浏览器或嵌入式 WebView,应在入口做能力检测,而不是等请求发生时才报错。

function supportsCombinedAbortSignal() {
  // 同时确认 any 与 timeout,避免只支持一半能力
  return typeof AbortSignal.any === "function"
    && typeof AbortSignal.timeout === "function";
}

if (!supportsCombinedAbortSignal()) {
  loadCompatibilityLayer(); // 由项目自己的兼容策略接管
}

手写兼容层时不仅要转发中止,还要保留第一个 reason,并在组合信号中止或任务结束后移除所有输入监听器。若项目没有旧环境需求,优先使用原生实现,避免重复维护边界语义。

验证清单

  • 用户取消、页面离开和超时是否分别拥有独立来源?
  • 是否只把一个组合 signal 传给当前异步操作?
  • 传入时已有中止信号,操作是否会立即停止?
  • 捕获逻辑是否能区分 TimeoutError、AbortError 和真实网络错误?
  • 是否通过 reason 保留了首个取消来源,而不是依赖竞态布尔值?
  • 组合信号中止后,是否没有错误地假设其他控制器也已中止?
  • 自定义 abort 监听器是否在正常完成和异常完成时都被清理?
  • 下一次操作是否会创建全新的控制器和组合信号?

实现正确的判断标准是:任一来源都能及时停止当前任务;只有最先发生的原因决定错误分类;任务完成后不残留页面监听器;旧 signal 不会污染下一次操作;超时、正常取消和真正失败走不同的用户提示与监控通道。

常见问题

AbortSignal.any 会中止传入的其他 signal 吗?

不会。它只观察输入来源并中止返回的组合信号。其他控制器的状态不会被反向修改。

数组里已有 signal 中止会怎样?

返回的组合信号会立即中止,reason 取输入顺序中第一个已中止信号的原因。开始昂贵任务前可调用 throwIfAborted()。

组合信号中止后能重置吗?

不能。AbortSignal 是一次性的。重试请求时要创建新的控制器、超时信号和组合信号。

只使用 AbortSignal.timeout 不行吗?

如果唯一终止条件就是超时,可以直接使用。需要同时响应用户取消、页面生命周期或上层 signal 时,再使用 AbortSignal.any() 组合。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go sql.DB.Stats 中 WaitCount 持续增长说明什么Go sql.DB.Stats 中 WaitCount 持续增长说明什么
上一篇
Go sql.DB.Stats 中 WaitCount 持续增长说明什么
栗子漫画意见反馈入口在哪里?公开资料页的个人中心、邮箱与问题提交说明
下一篇
栗子漫画意见反馈入口在哪里?公开资料页的个人中心、邮箱与问题提交说明
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    325次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    382次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    376次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    342次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    167次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码