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

前端怎样合并多个取消信号

来源:17golang原创

时间:2026-10-18 02:34:45 490浏览 收藏

前端合并多个取消来源,可以把各个 AbortSignal 传给 AbortSignal.any([...]),再把返回的组合信号交给 fetch。任意来源取消,组合信号就会取消;这不是等待所有来源,也不是把多个 AbortController 合成一个能随意重置的控制器。

官方规范地址:https://dom.spec.whatwg.org/;API 说明:https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static。下文只讨论联合取消,不把它当成权限控制、服务端事务回滚或旧浏览器通用兼容库。代码未在本机执行。

需要保护的是这一次请求,而不是全局开关

假设预览面板允许用户取消,也会随页面退出而失效,还要求等待时间有限。要避免的是:用户已经放弃,旧请求仍把结果写回界面;或者一次请求取消后,后面的请求复用了已取消信号而立即失败。本文用假设场景解释风险,没有声称发生过真实事故。

先分清拥有者:用户控制器属于本次操作,页面控制器属于页面生命周期,超时控制器与计时器属于本次请求。调用方负责何时触发取消,业务函数只接收 signal,不应擅自取消页面内其他操作。过宽的共享范围可能让一个局部动作结束整页工作,过窄又会漏掉页面退出这一来源。

组合信号只表达“任意一个来源取消”

userSignal、pageSignal 和 deadline.signal 是输入来源;AbortSignal.any 产生组合 signal;fetch 接收这个 signal;reason 保存使组合信号取消的原因。这些对象的职责是静态关联,不代表某种固定的发生顺序。

三个取消来源与组合信号和请求的静态关系
图1:结构说明图。取消来源与请求消费分组分开阅读,任意输入都可影响组合信号,但不会反向取消其他输入。

如果构造时已经有输入取消,返回信号立即处于已取消状态;存在多个已取消输入时,按输入迭代顺序采用其中第一个已取消信号的原因。输入都未取消时,则由随后最先使组合信号取消的来源提供 reason。后来的取消不会把组合信号重新激活。

最小实现要包含原因识别和清理

下面为每次读取创建一个自有超时来源,方便请求完成后 clearTimeout。八秒只是示例参数,不是服务等级或通用安全值。/api/preview 是演示用同源接口名称,不代表站点存在这个接口。调用方收到 kind 为 ok 后,仍需确认当前界面是否继续接受该次请求结果。

async function readPreview(userSignal, pageSignal) {
  // 明确要求原生支持,不悄悄忽略某个取消来源。
  if (typeof AbortSignal.any !== "function") {
    throw new Error("当前环境不支持联合取消");
  }

  const deadline = new AbortController();
  const signal = AbortSignal.any([
    userSignal, pageSignal, deadline.signal,
  ]);
  const timer = setTimeout(() => {
    deadline.abort(new DOMException("预览等待超时", "TimeoutError"));
  }, 8000);

  try {
    // 已取消输入会得到已取消组合信号,避免继续发起业务读取。
    signal.throwIfAborted();
    const response = await fetch("/api/preview", { signal });
    if (!response.ok) {
      throw new Error("预览请求返回 HTTP " + response.status);
    }
    const data = await response.json();
    // 防止后续异步阶段已结束却继续接收取消后的结果。
    signal.throwIfAborted();
    return { kind: "ok", data };
  } catch (error) {
    // 只识别这次请求的取消原因,不把所有异常都吞作“用户取消”。
    if (signal.aborted && Object.is(error, signal.reason)) {
      return { kind: "cancelled", reason: signal.reason };
    }
    throw error;
  } finally {
    // 自己创建的计时器由自己清理,成功、失败或取消都覆盖。
    clearTimeout(timer);
  }
}

// 每次请求新建用户控制器;页面控制器由页面生命周期的拥有者管理。
const userController = new AbortController();
const pageController = new AbortController();
// 只展示调用入口,按需调用,示例没有自动发出网络请求。
// readPreview(userController.signal, pageController.signal);
// 用户取消时调用 userController.abort(new DOMException("用户取消", "AbortError"));
// 页面退出时由页面拥有者调用 pageController.abort(new DOMException("页面退出", "AbortError"));

示例把 AbortSignal.any 作为能力门槛,throwIfAborted 也属于所需 API。面对旧运行环境,应在部署前明确目标支持范围,或使用单独维护且经过适用环境测试的兼容方案;不能仅删除 any 后保留一个来源而不告知调用方取消语义已经改变。

取消原因可以是任意 JavaScript 值,不能假设总有 name 字段;示例选择 DOMException 只是便于调用方展示。在捕获处比较这次 signal.reason,可以减少把网络异常或 HTTP 错误误报成用户取消。外层仍应处理抛出的非取消错误,不能因为函数使用了取消信号就完全忽略异常。

取消不是回滚,也不替你管理所有来源

组合信号取消不会替其他输入调用 abort,也不会自动清理它们背后的定时器。示例 finally 清掉自有 timer,就是为了不让一次成功请求留下无需再触发的计时器。若改用 AbortSignal.timeout,则不能把它当成可 clearTimeout 的句柄;它的时间语义还涉及活动时间,不能直接宣传成精确的服务器截止时间。

fetch 接收取消信号,并不意味着所有自定义 Promise 都会遵守它;新增的异步任务必须主动支持信号或在关键位置检查状态。请求到达服务端后,客户端取消也不证明服务端没有执行操作。涉及写入仍要另行处理业务幂等和结果确认,不能用联合取消代替事务。

取消状态中的 aborted 与 reason 归属于组合信号;自有 timer 则由 finally 清理;源控制器保持自己的生命周期。这三个边界应分别考虑。手动注册的 abort 监听器,也要在操作结束时移除,once 只保证事件发生时调用一次,不等于成功完成时自动解除所有关联。

取消状态、原因与自有计时器清理职责
图2:结构说明图。取消状态和资源清理分别分组,强调 reason 是状态信息,而 finally 负责自有 timer;不是运行证据。

把边界检查放到具体场景里

// 无网络的小例子:两个来源不会互相取消。
const left = new AbortController();
const right = new AbortController();
const firstReason = new DOMException("不再需要结果", "AbortError");
const joined = AbortSignal.any([left.signal, right.signal]);
left.abort(firstReason);

// 按规范推导应依次为 true、true、false;本文未执行。
console.log(joined.aborted);
console.log(joined.reason === firstReason);
console.log(right.signal.aborted);
场景应关注的结果
调用前输入已取消组合信号已取消,业务不继续读取
用户取消保留用户来源的 reason,不误标为超时
超时来源取消组合取消并清理自有 timer
请求正常完成finally 仍清理 timer,不复用这次请求状态
网络或 HTTP 失败保留非取消错误,交由上层处理

这些是读者可实施的检查项,不是本文测试报告。记录原因时只保留必要类别,避免把对象内容、令牌或完整请求数据直接写到日志。它能帮助定位“谁先取消”,却不是完整审计系统。

能不能等所有来源都取消才停止?

any 表达任意来源取消,并不实现“全部取消”。如果业务要求全部满足才终止,需要另行定义状态组合,不应给 any 加一个误导性函数名。

组合 signal 能调用 abort 吗?

不能把信号当作控制器。触发取消应通过拥有的 AbortController;组合信号负责传递取消状态。

下一次请求能继续复用已经取消的 signal 吗?

不适合。取消不可重置。下一次操作新建请求级控制器;若页面级信号已取消,也应重新评估页面生命周期,而不是继续把它放入新组合。

联合取消的核心是明确来源拥有者、给消费 API 传入组合信号,并处理原因与资源收尾。它减少重复监听代码,但不会自动完成权限、服务端回滚或所有异步工作的生命周期管理。

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