[转] 异步任务取消机制
异步任务取消机制
作者:谢杰
该文章是并发异步操作系列文章第一篇。
为什么需要取消异步任务
在现代的 Web 和 Node.js 应用中,我们经常需要启动一些耗时较长的异步任务,比如:
- 下载大文件
- 进行高强度的计算
- 持续监听一个长时间的事件流
然而,一旦任务开始运行,传统上我们只能等待它结束或因异常而中断。在很多情况下,这并不理想,因为我们可能需要主动终止任务,例如:
- 用户已经切换到其他页面,不再关心结果
- 请求超时
- 用户点击了“取消”操作
在早期的 Web API 中,XHR 提供过 .abort() 方法,但基于 Promise 的 fetch() 等 API 最初并没有类似的能力。
为了填补这一空白,WHATWG 在 2017 年将 AbortController 和 AbortSignal 纳入 DOM Standard,作为统一的取消信号机制。
和早期的 XHR 的实现所不同的是,早期的 XHR 里的 .abort() 方法,这是 XHR 自己实现的取消逻辑,其他 API 用不了。AbortController 则是 WHATWG 定义的统一标准,任何 API 只要愿意支持,都可以接收它的 signal 来实现取消(例如 fetch、ReadableStream、Node.js 的 fs.readFile、setTimeout 等等)。
虽然它最初出现在浏览器端,但这套设计与 DOM 并非深度耦合,只依赖于事件派发机制,因此 Node.js 自 v15.0.0(2020 年)起原生支持了 AbortController 与 AbortSignal(低于 v15 的版本可通过 node-abort-controller 等 polyfill 实现兼容)。如今,你可以在 Node 环境下的 fetch()、stream、timers 等异步 API 中,使用与浏览器一致的取消方案。
快速上手
接下来我们来看一下核心 API,有两个:
- AbortController
- AbortSignal
为啥会有两个呢?来看下面的图:
上面的示意图展示了 AbortController 与 AbortSignal 的工作原理。
在没有取消机制时(上图),客户端发起异步请求后,只能一直等待服务器完成耗时处理,即便中途不再需要结果,也无能为力。
引入取消机制后(下图),AbortController 就像一只“遥控器”,可以在任务执行过程中随时发出 AbortSignal。异步任务收到信号后,会立即停止执行,从而避免无意义的等待和资源消耗。
你可以将这套设计理解为:
- Controller:信号的发射端
- Signal:信号的接收端
这种模式让异步任务的生命周期可控,在请求超时、用户主动取消或页面切换等场景下,都能优雅、安全地终止任务。
下面来看一下代码层面具体该怎么写:
// 创建一个 AbortController 实例,用于管理和发出取消信号
const controller = new AbortController();
// 从控制器中获取对应的 AbortSignal 对象
// 这个 signal 会被传递给需要支持取消的 API
const signal = controller.signal;
// 使用 fetch 发送请求,并在配置中传入 signal
// 一旦 signal 被触发(abort),fetch 会立即中止
fetch("https://example.com", { signal })
.then(res => {
// 当请求正常完成时会进入这里
console.log("请求成功");
})
.catch(err => {
// 如果是因为调用了 controller.abort() 而中断,会返回 AbortError 类型的错误
if (err.name === "AbortError") {
console.log("请求被中断");
} else {
// 其他类型的错误,例如网络错误或服务器返回非 2xx 状态码
console.error("请求发生错误", err);
}
});
// 模拟延迟 2 秒后手动中止请求
// 当执行 controller.abort() 时,会触发 signal 的中止状态
// 已经关联了这个 signal 的 fetch 请求会被立即终止
setTimeout(() => controller.abort(), 2000);
在这个示例中,我们用最简单的方式演示了 AbortController 和 AbortSignal 如何配合 fetch() 实现“可中途取消”的网络请求。
首先,我们创建了一个 AbortController 实例,它就像任务的“遥控器”,专门用来发出取消的指令。接着,通过 controller.signal 获取与之绑定的 AbortSignal 对象。这个 signal 会被传递给支持取消机制的 API(例如 fetch),作为任务的监听端。
当 fetch 启动后,它会一直监听这个 signal 的状态。如果我们在任务执行中调用了 controller.abort(),signal 会立即变为“中止”状态,fetch 也会立刻终止请求,并抛出一个 AbortError。
在这段代码里,我们用 setTimeout 模拟了一个“2 秒后取消请求”的场景——这在实际开发中可能对应着请求超时、用户点击取消按钮、或页面切换等情况。
这种模式的好处是,你可以用一套统一的方式去管理异步任务的生命周期,不需要依赖特定 API 提供的私有取消方法,从而让取消逻辑更优雅、更易维护。
相关细节
在了解了 AbortController 的核心理念后,接下来我们来看一下该 API 一些细枝末节的知识。
前面我们聊了 AbortController 的用途和基本用法,但如果你真的要在项目里用得顺手,还是得知道它有哪些“开关”和“按钮”。这一节,我们就来扒一扒它的细节——属性、方法、甚至是那些看似不起眼的静态方法。
1. controller.signal
这是 AbortController 唯一的属性,也是它存在的核心意义。
你可以把它理解为 “传话的麦克风”——只要控制器这边说“停”,所有拿着这个 signal 的任务都会听到。
- 类型:
AbortSignal - 常用场景:传给
fetch()、ReadableStream、setTimeout等支持取消的 API。
const controller = new AbortController();
const { signal } = controller;
fetch("/api/data", { signal })
.then(r => r.json())
.then(console.log)
.catch(err => {
if (err.name === "AbortError") console.log("被取消了");
else console.error(err);
});
// 3 秒后取消
setTimeout(() => controller.abort("timeout"), 3000);
2. controller.abort([reason])
这是 AbortController 最常用的方法,也是你按下“终止任务”按钮的地方。
-
作用:触发
signal的abort事件,让关联的异步任务立刻中止。 -
可选参数
reason:可以传一个自定义原因,比如"Timeout"或一个Error对象,这样在捕获时就能知道为什么被中止。注意点:调用
controller.abort()一次就够了,多次调用没有额外效果(signal.aborted会保持true)。
const c = new AbortController();
const s = c.signal;
fetch("/slow", { signal: s }).catch(err => {
// 在现代浏览器/Node 里,err.name === "AbortError"
// 同时 err.message / s.reason 里能拿到“取消原因”
if (s.aborted) {
console.log("已取消,原因:", s.reason); // e.g. "user-cancel" or Error(...)
}
});
document.getElementById("cancel").addEventListener("click", () => {
c.abort("user-cancel");
});
3. 静态方法 AbortSignal.abort(reason)
听到 AbortSignal.abort() 这个名字,你可能会疑惑:它和实例方法 controller.abort() 到底有什么不同?
区别在于:AbortSignal.abort() 是一个快捷工厂——直接生成一个已经处于中止状态的 AbortSignal,免去了你先 new AbortController() 再手动调用 abort() 的步骤。
它非常适合那种一开始就确定要中止的场景,比如你在写一个工具函数时,发现条件不满足,就立即返回一个“无效”的信号,让后续逻辑立刻停下。
来看个例子:假设我们有一个支持取消的文件下载工具函数:
async function downloadFile(url, signal) {
// 检查这个 AbortSignal 是否已经处于中止状态
// 如果已中止,立即抛出 AbortError,避免发起无意义的请求
// 这是 AbortSignal 提供的同步检查方法(现代浏览器/Node.js 均支持)
signal.throwIfAborted();
const res = await fetch(url, { signal });
return res.blob();
}
现在有个需求:
- 用户必须登录才能下载文件
- 如果未登录,不仅要阻止下载,还要避免发起任何网络请求
用 AbortSignal.abort() 就能优雅地实现:
function getDownloadSignal(isLoggedIn) {
if (!isLoggedIn) {
// 创建一个“已中止”的 signal,原因是未登录
return AbortSignal.abort(new Error("User not logged in"));
}
const controller = new AbortController();
return controller.signal;
}
// 模拟未登录
const signal = getDownloadSignal(false);
// 这里会立刻抛出 AbortError
downloadFile("/big-file.zip", signal)
.then(() => console.log("下载完成"))
.catch(err => {
if (err.name === "AbortError") {
console.log("下载被取消:", signal.reason);
} else {
console.error(err);
}
});
这样做的好处:
- 零浪费:不会发送无意义的请求。
- 调用方逻辑一致:依旧通过捕获
AbortError处理取消,无需特殊分支。 - 写法简洁:少了创建控制器再手动
abort()的步骤。
4. 静态方法 AbortController.timeout(ms)
这是 Node.js 从 v15.4.0 开始加的“便捷版”(浏览器端目前大部分不支持,需自行 polyfill)。
- 作用:创建一个
AbortSignal,并在指定的毫秒数后自动进入中止状态。 - 优势:不用自己写
setTimeout再调用abort(),更简洁。 - 典型场景:请求超时控制,比如
fetch(url, { signal: AbortController.timeout(5000) })。
// 5 秒超时:到点自动进入 aborted 状态
const signal = AbortSignal.timeout(5000);
fetch("/maybe-slow", { signal })
.then(r => r.text())
.then(console.log)
.catch(err => {
if (err.name === "AbortError") {
console.log("超时了");
} else {
console.error(err);
}
});
总的来讲,AbortController 的设计非常简洁。
唯一的属性是 signal,唯一的实例方法是 abort()。
但配合 AbortSignal,它能在异步任务中提供一致、优雅的取消能力。再加上静态方法的加持,你既能自己掌控中止时机,也能用“一键定时取消”快速处理超时场景。
进阶思考
设想这样一个场景:你在网页上点击了“删除数据库记录”,请求很快发了出去,服务器那边的 SQL 语句已经开始准备执行。突然,你反悔了,立刻调用 controller.abort()。此时,这个删除动作真的还能被阻止吗?
现实可能会让你有些失望——大多数情况下,答案是否定的。
AbortController 的取消逻辑,其实只作用在客户端,它能做到的包括:
- 让浏览器立即停止发送或接收数据
- 让绑定了该信号的异步任务(如
fetch())立刻结束,并抛出AbortError - 丢弃本地的任务结果,不再进行后续处理
但是,一旦请求已经抵达服务器并开始执行,客户端的中止信号并不能“远程撤销”后端的操作。换句话说,删除数据库的 SQL 语句可能已经在运行,你的 abort() 并不能让它立刻停下来。
这就像你在餐馆点了一份大餐,订单已经送到厨房,厨师正忙着下锅。这时你接到一个急事电话,必须马上离开——虽然你对服务员说“不用了”,但厨房那边其实已经在做,这道菜并不会因为你离开而自动停下。
如果真想实现全链路取消,就必须有后端的配合——在任务执行过程中检查取消信号,并在检测到时主动中止操作。
因此,AbortController 在前端任务管理中非常实用,但要想让它变成“全局任务终止按钮”,前后端需要协同设计取消机制。
写在最后
在现代前后端开发中,异步任务的可控性越来越重要。无论是浏览器端的 fetch() 请求、WebSocket 长连接,还是 Node.js 中的文件流、定时器任务,能够在合适的时机中止它们,意味着更好的资源管理和更流畅的用户体验。
AbortController 与 AbortSignal 为我们提供了一套统一且优雅的取消机制——它不依赖具体 API 的私有实现,而是通过“控制器 → 信号”的模式,让不同类型的异步任务都能用相同的方式进行管理。这不仅降低了心智负担,也让取消逻辑更容易抽象成可复用的工具。
当然,我们也要认识到它的边界:取消信号只在客户端生效,并不能直接干预已经在服务器上执行的逻辑。如果需要真正的“全链路取消”,就必须在后端配合检查取消状态,并在合适的时机主动中止处理。
因此,在实际项目中,你可以把 AbortController 看作是异步任务的本地总开关:
- 它能帮你优雅地处理请求超时、用户主动取消、页面卸载等场景;
- 它能统一异步任务的生命周期管理方式;
- 它能减少资源浪费,避免无意义的等待和处理。
熟练掌握并恰当使用 AbortController,会让你的异步代码不仅能“跑起来”,更能“收得住”。
-EOF-

浙公网安备 33010602011771号