如何优雅打断 JS 任务?AbortController 正确使用方式

887 阅读3分钟

前端开发者常常需要处理“取消任务”的场景:接口请求发出后用户快速切换页面、组件卸载、搜索防抖中断上一次请求等等。这种需求本质上就是**“任务中断”**。

但在过去,JavaScript 没有原生取消异步任务的能力。我们通常要么靠手动标记变量、要么写一些不太优雅的 hack 逻辑。但现在有了 AbortController,一切变得清爽、现代、标准化。

这篇文章将带你系统掌握 AbortController,包括:

  • 它究竟能做什么?
  • 如何优雅地用它终止请求?
  • 实际应用中的注意事项与踩坑点
  • 它还能“终止”谁?

为什么我们需要 AbortController?

先抛个现实问题:假如你在搜索框中绑定了一个 fetch 请求,每次输入都会触发新的请求。

input.addEventListener('input', (e) => {
  fetch(`/search?q=${e.target.value}`)
    .then(res => res.json())
    .then(data => updateResults(data))
})

用户快速输入,连续触发了五六个请求,但服务器可能按顺序返回。这就出现了经典问题:

后发请求先返回,覆盖了正确的结果。

为了避免这个问题,你需要中止前一个请求。过去我们要靠自己管理“状态标记”或是 Axios 的取消令牌(现在也废弃了)。而现在,AbortController 原生就能干这事。


AbortController 基本用法

const controller = new AbortController()
const signal = controller.signal

fetch('/api/data', { signal })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求被取消了')
    } else {
      console.error('其他错误', err)
    }
  })

// 某个时机触发取消
controller.abort()

你可以把这个逻辑想象成:

把任务和一个“遥控器”绑在一起,某个时刻按下“停止”按钮,就可以中止它。


AbortController 应用场景全集

✅ 1. 取消多次搜索请求(防抖、节流场景)

let controller = null

function search(keyword) {
  if (controller) controller.abort() // 中断上一次

  controller = new AbortController()

  fetch(`/search?q=${keyword}`, { signal: controller.signal })
    .then(res => res.json())
    .then(updateUI)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err)
    })
}

注意:controller 每次都重新生成,前一个就自然失效。这是处理用户快速输入时的黄金搭配。


✅ 2. 组件卸载时中止异步任务(React/Vue)

useEffect(() => {
  const controller = new AbortController()

  fetch('/api/data', { signal: controller.signal })
    .then(...)
    .catch(...)

  return () => {
    controller.abort()
  }
}, [])

React 中常用在 useEffect 的清理阶段,避免组件卸载后仍处理异步结果(比如 setState 报错)。

Vue 中也可以结合 onUnmounted 实现类似效果。


✅ 3. 并发任务中断所有子任务(超时处理)

const controller = new AbortController()

const timeout = setTimeout(() => {
  controller.abort()
}, 5000)

Promise.all([
  fetch('/a', { signal: controller.signal }),
  fetch('/b', { signal: controller.signal })
]).then(...).catch(...)

这种写法可以让你设置一个总超时时间,一旦超时就同时中断多个请求


✅ 4. 自定义异步任务支持取消

不仅仅是 fetch,你还可以让你自定义的异步任务也响应 AbortController

function wait(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms)

    signal?.addEventListener('abort', () => {
      clearTimeout(id)
      reject(new DOMException('Aborted', 'AbortError'))
    })
  })
}

const controller = new AbortController()
wait(3000, controller.signal).then(() => {
  console.log('Done')
}).catch(err => {
  if (err.name === 'AbortError') console.log('任务中断')
})

只要你自己监听 abort 事件,你就能让任何异步任务“听话地终止”。


冷知识:AbortController 不止用于 fetch

大部分人只用它和 fetch 配合,但其实 任何支持 signal 的 API 都能使用,比如:

  • ReadableStream
  • WebSocket(部分 polyfill 支持)
  • 自定义异步任务(如上)
  • 一些 Web Worker 协议中也逐步加入支持

随着 Web API 的不断完善,未来会有更多异步 API 原生支持 AbortSignal


实战技巧与常见坑

☠️ 1. AbortController 不是“可重用”的!

它只能用一次。调用 .abort() 之后,它就“废了”。你需要为每次任务生成一个新的 controller

☠️ 2. 没传 signal?那就是无效取消

你一定要把 controller.signal 显式传给任务,否则调用 .abort() 是无效的。

☠️ 3. 捕获错误时必须判断 AbortError

.catch(err => {
  if (err.name === 'AbortError') {
    // 被取消的,不是 bug
  } else {
    throw err // 真正的异常应该继续抛出
  }
})

否则你会误把取消当作程序异常处理。


如何优雅封装一个支持中断的异步请求?

假设你有一个搜索请求模块,可以封装成这样:

function createAbortableSearch() {
  let controller = null

  return function search(keyword) {
    if (controller) controller.abort()

    controller = new AbortController()

    return fetch(`/search?q=${keyword}`, { signal: controller.signal })
  }
}

const search = createAbortableSearch()

search('vue')
search('vue3')
search('vue3 composition') // 只会保留最后一次请求

这种写法非常适合抽象在业务组件之外,让组件“只负责输入”,而不关心取消细节。


结语:AbortController 是现代前端的必修课

在现代 Web 中,异步操作越来越多、任务流越来越复杂。任务取消不再是“边角料功能”,而是现代 Web 应用“响应性”和“健壮性”的关键。

AbortController 带来了标准化的取消机制,优雅、清晰、可组合。掌握它,你就能:

  • 写出更优雅的异步代码
  • 更好地控制资源与生命周期
  • 减少无效请求与副作用