前端请求取消:用 AbortController 从 fetch 到 axios

前端开发里,页面切换、用户重复操作,都可能让尚未完成的请求变成"孤儿"。这些请求如果继续影响当前页面,数据和 UI 状态就容易变得不可控。

取消这些无效请求,可以避免旧结果继续影响当前页面。请求取消本身不难:实例化一个 AbortController,把 signal 传给请求,需要取消时调用 abort()。难的是,真实项目里请求通常会经过 axios 实例、二次封装的 axios、业务 API,最后才到页面组件。那么 AbortController 应该在哪里创建?signal 要怎么传?多个请求又该怎么处理?

下面从原生 fetch 开始讲解,再过渡到已经封装好的 axios 业务接口。

为什么要取消请求

例如,页面存在未完成的请求时用户跳转,旧请求可能引发几个问题:

  • 请求返回后还想更新一个已经卸载的组件。
  • 旧页面的数据被写回某个共享状态。
  • 用户重复点击,前一个慢请求比后一个快请求更晚返回,导致数据被覆盖。
  • 弹窗已经关闭了,但弹窗里的请求还在跑。

取消请求不是为了保证服务端一定停止执行。请求发出去之后,服务端可能已经收到,也可能已经处理完了。前端调用 abort(),其实是在告诉当前这次请求:我这边不等了,这个结果也不应该继续影响当前页面。

核心:AbortController

首先了解一下 AbortController

js 复制代码
const controller = new AbortController()

controller.signal

controller.abort()

AbortController 是控制器。

controller.signal 是取消信号,传给请求。

controller.abort() 是发出取消通知。

注意:controller 调用过 abort() 之后,就不能复用了。

因为它的 signal.aborted 会变成 true,而且不会再变回 false。如果你把一个已经 aborted 的 signal 传给新的请求,新请求会立刻被取消。

fetch 里的取消请求

取消单个请求

先用 fetch 演示最小流程。

js 复制代码
const controller = new AbortController()

async function getUserList() {
  try {
    await fetch('/api/users', {
      // 把 signal 交给 fetch,这个请求才可以被 abort 控制
      signal: controller.signal,
    })

    console.log('请求成功')
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('请求已取消')
      return
    }

    console.log('请求失败', error)
  }
}

getUserList()

// 需要取消时调用
controller.abort()

取消时调用 controller.abort(),这次请求就会收到取消信号。

注意在 catch 中区分错误类型。fetch 被取消后会抛出 AbortError,它不是普通业务错误。页面上最好不要把它显示成"请求失败",否则用户会看到一个并不真实的错误提示。

批量取消

如果多个请求要一起取消,给它们传同一个 signal 就可以。

js 复制代码
const controller = new AbortController()

async function getPageData() {
  try {
    await Promise.all([
      fetch('/api/users', {
        signal: controller.signal,
      }),
      fetch('/api/roles', {
        signal: controller.signal,
      }),
    ])

    console.log('页面数据请求成功')
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('这一组请求已取消')
      return
    }

    console.log('请求失败', error)
  }
}

getPageData()

// 一个 abort 会取消所有共用这个 signal 的请求
controller.abort()

这种写法适合一组请求要一起取消的场景。比如弹窗关闭后,弹窗里的几个请求都不需要了。

进入实际开发

前面的例子直接写 fetch,是为了看清楚 AbortController 本身怎么工作。

实际开发里更常见的是这样:

txt 复制代码
页面组件
  -> getUserList()
    -> http.get()
      -> axios 实例

这里的 getUserList 是已经封装好的业务请求接口。先关心两件事:

  • 它可以接收业务参数,比如分页、筛选条件。
  • 它也可以接收 signal,内部会把这个 signal 继续传给 axios 实例。

业务 API 内部怎么拆参数、怎么调用 http.get,不同项目写法不一样,这里不展开讲。本文只关心 signal 怎么传下去:页面创建 controller,把 controller.signal 传给业务 API,业务 API 再把它交给 axios。

普通业务调用

先看不取消请求时,业务 API 是怎么用的,方便理解后面代码。

js 复制代码
async function loadUsers() {
  const users = await getUserList({
    page: 1,
    pageSize: 10,
  })

  console.log(users)
}

getUserList 本身还是普通的业务请求函数。后面要取消请求,只是在这个调用基础上多传一个 signal

单个 axios 请求取消

现在给这个调用加上取消能力。到了页面组件里,还要考虑组件卸载时清理请求。下面用 Vue 3 举例。

具体流程是:

  1. 发请求前创建新的 AbortController
  2. 调用业务 API 时传入 signal
  3. 取消时调用 controller.abort()
  4. 组件销毁前也调用一次取消,避免页面离开后旧请求继续影响页面。

代码示例:

注意:为了方便演示,这里没有展开重复点击、旧请求回调晚返回这些边界处理。

js 复制代码
import { onUnmounted, ref } from 'vue'
import axios from 'axios'
import { getUserList } from '@/api/user'

const users = ref([])
const status = ref('idle')
let controller = null

async function loadUsers() {
  controller = new AbortController()
  status.value = 'loading'

  try {
    users.value = await getUserList({
      page: 1,
      pageSize: 10,
      // signal 会一路传到 axios 请求配置里
      signal: controller.signal,
    })

    status.value = 'success'
  } catch (error) {
    // axios 取消请求会进入 catch,需要单独区分
    if (axios.isCancel(error)) {
      status.value = 'aborted'
      return
    }

    status.value = 'error'
  } finally {
    controller = null
  }
}

function cancelRequest() {
  controller?.abort()
}

onUnmounted(() => {
  // 跳转页面或组件销毁时,清理还没完成的请求
  cancelRequest()
})

前面说过,调用过 abort() 的 controller 不能复用,因为它的 signal.aborted 已经是 true。所以每次重新请求前,都要 new 一个新的 AbortController

取消请求和请求失败也要分开处理。取消是 aborted,失败才是 error

多请求:一个 signal 取消多个请求

如果页面里有一组请求要一起取消,可以让它们共用一个 controller。

下面这个例子里,用户列表和角色列表共用同一个 signal,权限列表不传 signal。也就是说,取消时只取消前两个请求,权限列表不受影响。

js 复制代码
import axios from 'axios'
import { getPermissionList, getRoleList, getUserList } from '@/api/user'

let controller = null

async function loadPageData() {
  cancelPageData()

  const currentController = new AbortController()
  controller = currentController

  try {
    // 这两个请求共用一个 signal,取消时会一起取消
    const usersPromise = getUserList({
      signal: currentController.signal,
    })

    const rolesPromise = getRoleList({
      signal: currentController.signal,
    })

    getPermissionList()
      .then(permissions => {
        console.log('权限请求成功', permissions)
      })
      .catch(error => {
        console.log('权限请求失败', error)
      })

    const [users, roles] = await Promise.all([
      usersPromise,
      rolesPromise,
    ])

    if (controller === currentController) {
      console.log(users, roles)
    }
  } catch (error) {
    if (axios.isCancel(error)) {
      if (controller === currentController) {
        console.log('用户和角色请求已取消')
      }
      return
    }

    console.log('请求失败', error)
  } finally {
    if (controller === currentController) {
      controller = null
    }
  }
}

function cancelPageData() {
  controller?.abort()
}

这里用户和角色属于同一个取消分组,所以放进同一个 Promise.all。权限请求不传 signal,也不放进这个 Promise.all,而是单独处理。这样用户和角色被取消时,权限请求不会被 abort,它自己的结果也不会被这个取消分支跳过。

这个写法有一个缺陷:一旦调用 controller.abort(),这个 controller 就不能再用了。下一轮请求必须重新创建新的 controller。

多请求:可更新的共享 signal

上面的例子把一组请求都放在 loadPageData() 里,controller 的创建和清理都在一个地方。实际页面里,请求经常分散在不同函数中:用户列表在 loadUsers 里,角色列表在 loadRoles 里,但它们又想进入同一个取消分组。

这种情况下可以写一个 getOrCreateSharedSignal()。每次发请求前先拿 signal;如果旧 controller 已经取消了,就创建一个新的。

js 复制代码
import axios from 'axios'
import { getRoleList, getUserList } from '@/api/user'

let sharedController = null

function getOrCreateSharedSignal() {
  if (!sharedController || sharedController.signal.aborted) {
    // 旧 controller 已经 abort 后不能复用,所以这里要重新创建
    sharedController = new AbortController()
  }

  return sharedController.signal
}

async function loadUsers() {
  try {
    const users = await getUserList({
      signal: getOrCreateSharedSignal(),
    })

    console.log(users)
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('用户请求已取消')
      return
    }

    console.log('用户请求失败', error)
  }
}

async function loadRoles() {
  try {
    const roles = await getRoleList({
      signal: getOrCreateSharedSignal(),
    })

    console.log(roles)
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('角色请求已取消')
      return
    }

    console.log('角色请求失败', error)
  }
}

function cancelGroupRequests() {
  sharedController?.abort()
  sharedController = null
}

这段代码处理的是"共享 signal 取消后不能复用"的问题。取消之后把旧 controller 丢掉,下一次调用 getOrCreateSharedSignal() 时会自动创建新的 controller。

多请求:一个请求一个 controller

除了在共享 signal 方案里重新创建 controller,也可以换个思路:一个请求一个 controller。页面维护一个请求队列,需要取消时统一处理。

这种方式适合请求生命周期不完全一致的页面。需要取消的请求放进队列,不需要取消的请求正常调用。

js 复制代码
import axios from 'axios'
import { getPermissionList, getRoleList, getUserList } from '@/api/user'

const controllers = new Map()

function createController(key) {
  // 只有需要取消的请求才登记到队列里
  controllers.get(key)?.abort()

  const controller = new AbortController()
  controllers.set(key, controller)
  return controller
}

function removeController(key, controller) {
  // 只清理当前请求自己的 controller,避免误删后发起的新请求
  if (controllers.get(key) === controller) {
    controllers.delete(key)
  }
}

async function loadUsers() {
  const controller = createController('users')

  try {
    const users = await getUserList({
      signal: controller.signal,
    })

    console.log(users)
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('用户请求已取消')
      return
    }

    console.log('用户请求失败', error)
  } finally {
    removeController('users', controller)
  }
}

async function loadRoles() {
  const controller = createController('roles')

  try {
    const roles = await getRoleList({
      signal: controller.signal,
    })

    console.log(roles)
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('角色请求已取消')
      return
    }

    console.log('角色请求失败', error)
  } finally {
    removeController('roles', controller)
  }
}

async function loadPermissions() {
  // 这个请求不需要取消,所以不创建 controller,也不传 signal
  const permissions = await getPermissionList()
  console.log(permissions)
}

function cancelAllRequests() {
  // 页面离开时遍历队列,批量取消还没完成的请求
  controllers.forEach(controller => {
    controller.abort()
  })

  controllers.clear()
}

页面卸载时统一取消:

js 复制代码
import { onUnmounted } from 'vue'

onUnmounted(() => {
  cancelAllRequests()
})

队列写法代码多一点,但更灵活。真实页面里如果有很多接口,只有一部分需要取消,用队列会比手写多个 controller.abort() 更清楚。

总结

controller 一般放在页面或业务流程里创建,signal 传给业务 API,再由业务 API 继续传给 axios。调用过 abort() 的 controller 不能复用,因为它的 signal.aborted 已经是 true

单个请求就单独 new controller;一组请求要一起取消,就共用 signal;如果页面请求多、生命周期不一样,就一个请求一个 controller,再用队列统一取消。取消请求不是业务失败,UI 上不要把它当成普通 error 展示。

参考

  1. JS-AbortController:优雅中止请求操作在前端开发中,我们经常遇到需要中途撤回请求的情况(例如:搜索框快速 - 掘金

首发地址:https://blog.xchive.top/2026/cancel-requests-with-abort-controller.html

相关推荐
Python大数据分析@10 小时前
HTML会代替Markdown吗?为什么?
前端·html
一棵树735110 小时前
js总结介绍
前端·javascript·html
jiayong2310 小时前
前端面试题库 - 工程化与性能优化篇
前端·面试·性能优化
暗冰ཏོ10 小时前
2026前端开发资源大全:工具、文档、框架、学习路线与实战指南
前端·前端开发工具·前端编程工具·前端资源·前端开发文档
踩着两条虫10 小时前
AI 低代码引擎可视化设计器交互机制实战
前端·vue.js·人工智能·低代码·架构
ZC跨境爬虫10 小时前
跟着 MDN 学CSS day_2:(连接样式表与选择器的实战艺术)
java·前端·css·ui·html·媒体
lichenyang45310 小时前
鸿蒙聊天 Demo 练习 01:发送消息、模拟 AI 回复与自动滚动
前端
放下华子我只抽RuiKe511 小时前
React 从入门到生产(三):副作用与数据获取
前端·javascript·深度学习·react.js·开源·ecmascript·集成学习
微祎_11 小时前
写给前端的 CANN-ops-transformer:昇腾Transformer进阶算子库到底是啥?
前端·深度学习·transformer