📝 面试真题解析:前端项目中如何取消请求?

183 阅读9分钟

前端请求取消机制完整指南:原理、方案与Vue3最佳实践

一、请求取消的核心价值

1.1 必要性分析

  • 资源释放:避免无效请求占用网络带宽
  • 内存管理:防止未完成请求导致的内存泄漏
  • 数据一致性:消除过时数据覆盖最新结果的隐患
  • 用户体验:提升页面响应速度,减少无效等待

1.2 典型应用场景

场景类型具体案例技术需求
页面导航路由切换时取消未完成请求全局请求管理
动态搜索输入框连续触发搜索请求防抖+自动取消
数据视图切换选项卡快速切换请求优先级控制
大文件传输用户主动取消上传/下载中断数据传输流
超时处理自动终止长时间未响应请求超时监控机制

二、技术实现方案对比

2.1 原生方案实现

// XMLHttpRequest实现
const xhr = new XMLHttpRequest()
xhr.open('GET', '/api/data')
xhr.send()

// 取消请求
xhr.abort()

// Fetch + AbortController
const controller = new AbortController()
fetch('/api/data', {
  signal: controller.signal
})
controller.abort()

2.2 Axios演进方案

// 传统CancelToken
const source = axios.CancelToken.source()
axios.get('/api/data', {
  cancelToken: source.token
})
source.cancel('用户取消操作')

// 现代AbortController
const controller = new AbortController()
axios.get('/api/data', {
  signal: controller.signal
})
controller.abort()

方案对比表

特性XMLHttpRequestFetch APIAxios CancelTokenAxios AbortController
兼容性IE7+现代浏览器所有环境需要polyfill
Promise支持需封装原生支持支持支持
取消错误类型AbortErrorCancel对象Cancel对象
主动终止能力立即立即立即立即
请求进度监控支持不支持支持支持

三、Vue3深度实践指南

3.1 组件级实现

<script setup>
import { ref, watch, onUnmounted } from 'vue'
import axios from 'axios'

// 定义响应式搜索词变量,初始值为空字符串
const searchQuery = ref('')
// 定义响应式结果数组,初始为空数组
const results = ref([])
// 定义当前控制器变量,用于存储AbortController实例
let currentController = null

// 组件卸载时的生命周期钩子
// 用于在组件销毁时自动取消未完成的请求
onUnmounted(() => {
  // 可选链操作符(?.)确保currentController存在时才调用abort()
  currentController?.abort()
})

// 监听searchQuery的变化
watch(
  searchQuery,  // 监听的响应式变量
  (newVal) => {  // 变化回调函数,newVal是新值
    // 1. 取消前一个未完成的请求(如果存在)
    currentController?.abort()
    
    // 2. 创建新的AbortController实例
    // 每个新请求都需要新的控制器,因为abort()后控制器不可重用
    currentController = new AbortController()
    
    // 3. 发起新的axios请求
    axios.get('/api/search', {
      params: { q: newVal },  // 查询参数
      signal: currentController.signal  // 绑定取消信号
    })
    .then(res => {  // 请求成功处理
      // 将响应数据赋值给results
      results.value = res.data
    })
    .catch(err => {  // 请求错误处理
      // 只有当错误不是取消请求导致的,才输出错误日志
      if (!axios.isCancel(err)) {
        console.error('搜索失败:', err)
      }
      // 如果是取消请求导致的错误,静默处理不做任何操作
    })
  }, 
  { 
    debounce: 300  // 防抖配置,300ms内多次变化只触发最后一次
  }
)
</script>

3.2 可复用组合式函数

// useRequest.js
import { onUnmounted } from 'vue'

// 导出自定义Hook函数
export function useRequest() {
  // 创建一个Set集合来存储所有进行中的请求控制器
  // Set集合自动处理重复值,适合存储唯一控制器实例
  let pendingControllers = new Set()

  /**
   * 封装带取消功能的请求方法
   * @param {Object} config - axios请求配置
   * @returns {Promise} - 返回axios请求的Promise
   */
  const makeRequest = async (config) => {
    // 创建新的AbortController实例
    const controller = new AbortController()
    
    // 将控制器添加到pending集合中
    pendingControllers.add(controller)
    
    try {
      // 发起axios请求,将控制器的signal注入配置
      return await axios({
        ...config,          // 展开传入的配置
        signal: controller.signal  // 添加取消信号
      })
    } finally {
      // 无论请求成功或失败,都从集合中移除该控制器
      pendingControllers.delete(controller)
    }
  }

  /**
   * 取消所有进行中的请求
   */
  const cancelAll = () => {
    // 遍历所有控制器并执行取消操作
    pendingControllers.forEach(c => c.abort())
    
    // 清空控制器集合
    pendingControllers.clear()
  }

  // 组件卸载时自动取消所有请求
  // 避免组件卸载后请求完成导致的内存泄漏
  onUnmounted(cancelAll)

  // 返回对外暴露的方法
  return { 
    makeRequest,  // 带取消功能的请求方法
    cancelAll     // 手动取消所有请求的方法
  }
}

3.3 路由级管理

// router.js
import { createRouter } from 'vue-router'
import { useRequest } from './useRequest'

const router = createRouter({...})

router.beforeEach((to, from) => {
  const { cancelAll } = useRequest()
  // 取消所有进行中请求
  cancelAll()
})

四、高阶应用场景

4.1 批量请求池

/**
 * 请求池管理类 - 用于控制并发请求数量和取消请求
 * @param {number} maxConcurrent 最大并发数,默认5
 */
class RequestPool {
  constructor(maxConcurrent = 5) {
    // 请求等待队列(先进先出)
    this.queue = []
    // 活跃请求映射表(使用Map存储正在进行中的请求)
    this.active = new Map()
    // 最大并发请求数
    this.max = maxConcurrent
  }

  /**
   * 添加请求到池中
   * @param {Function} request 请求函数,需接收AbortSignal参数
   * @returns {Promise} 返回包装后的Promise
   */
  add(request) {
    // 为每个请求创建独立的AbortController
    const controller = new AbortController()
    // 为每个请求创建唯一标识
    const id = Symbol()
    
    // 返回新的Promise以便外部调用
    return new Promise((resolve, reject) => {
      // 将请求加入队列
      this.queue.push({
        id,          // 请求唯一标识
        request: () => request(controller.signal), // 包装请求函数
        controller,  // 关联的控制器
        resolve,     // 外部Promise的resolve
        reject       // 外部Promise的reject
      })
      // 尝试处理下一个请求
      this.processNext()
    })
  }

  /**
   * 处理队列中的下一个请求
   */
  processNext() {
    // 当活跃请求数未达上限且队列不为空时
    while (this.active.size < this.max && this.queue.length) {
      // 从队列头部取出任务
      const task = this.queue.shift()
      // 将任务加入活跃映射表
      this.active.set(task.id, task)
      
      // 执行实际请求
      task.request()
        .then(task.resolve)  // 成功时触发外部Promise的resolve
        .catch(task.reject)  // 失败时触发外部Promise的reject
        .finally(() => {
          // 无论成功失败,都从活跃表移除
          this.active.delete(task.id)
          // 继续处理下一个请求
          this.processNext()
        })
    }
  }

  /**
   * 取消特定请求
   * @param {Symbol} id 请求标识符
   */
  cancel(id) {
    // 在队列中查找对应请求
    const task = this.queue.find(t => t.id === id)
    if (task) {
      // 中止请求
      task.controller.abort()
      // 从队列中移除
      this.queue = this.queue.filter(t => t.id !== id)
    }
    // 注:活跃中的请求会自动触发reject,由processNext处理清理
  }
}

4.2 智能重试机制

/**
 * 带自动重试机制的fetch封装函数
 * @param {string} url - 请求URL
 * @param {Object} options - fetch选项配置
 * @param {number} retries - 最大重试次数,默认3次
 * @returns {Promise<Response>} - 返回fetch的响应Promise
 * @throws {Error} - 当重试耗尽或请求被取消时抛出错误
 */
const fetchWithRetry = async (url, options = {}, retries = 3) => {
  // 创建AbortController实例用于取消请求
  const controller = new AbortController()
  
  // 重试循环
  for (let i = 0; i < retries; i++) {
    try {
      // 发起fetch请求,注入取消信号
      const res = await fetch(url, {
        ...options,           // 合并传入的配置
        signal: controller.signal  // 添加取消控制
      })
      
      // 请求成功直接返回结果
      return res
      
    } catch (err) {
      // 判断是否应该终止重试:
      // 1. 已经是最后一次重试
      // 2. 错误是AbortError(请求被取消)
      if (i === retries - 1 || err.name === 'AbortError') {
        throw err  // 抛出原始错误
      }
      
      // 指数退避等待:1s, 2s, 3s...
      await new Promise(r => setTimeout(r, 1000 * (i + 1)))
    }
  }
}

五、工程化实践要点

5.1 错误处理规范

// 统一错误拦截器
axios.interceptors.response.use(null, (error) => {
  if (axios.isCancel(error)) {
    return Promise.reject({
      type: 'CANCELED',
      message: '请求已被取消',
      config: error.config
    })
  }
  
  // 其他错误处理逻辑
})

5.2 性能优化策略

  1. 请求去重:相同URL+参数的请求自动合并
  2. 缓存复用:GET请求结果缓存策略
  3. 优先级控制:关键请求优先处理
  4. 智能超时:动态调整超时时间
// 请求缓存示例
const cache = new Map()

const getWithCache = async (url) => {
  if (cache.has(url)) {
    return cache.get(url)
  }
  
  const res = await axios.get(url)
  cache.set(url, res.data)
  return res.data
}

5.3 调试技巧

  1. Chrome DevTools
    • Network面板过滤"Aborted"请求
    • 查看请求的initiator堆栈
  2. 控制台监控
    // 全局取消日志
    //`unhandledrejection` 是一个全局捕获 Promise 拒绝(rejection)的浏览器事件,当 Promise 被拒绝(reject)且没有被任何 `.catch()` 处理时触发。
    window.addEventListener('unhandledrejection', (event) => {
      if (event.reason?.type === 'CANCELED') {
        console.log('被取消的请求:', event.reason.config.url)
        event.preventDefault()
      }
    })
    

六、企业级解决方案

6.1 SSR兼容方案

// 服务端渲染适配
export const createRequestController = () => {
  if (process.server) {
    return {
      signal: null,
      abort: () => {},
      isServer: true
    }
  }
  return new AbortController()
}

6.2 TypeScript增强

/**
 * 可取消请求的扩展配置接口
 * 继承自 Axios 原有的请求配置类型
 */
interface CancellableRequestConfig extends AxiosRequestConfig {
  /**
   * 请求取消键 - 用于标识和分组可取消的请求
   * @description 
   * 1. 可通过此key批量取消同一类请求
   * 2. 例如设置为模块名或功能名: 'user-list' 或 'upload'
   */
  cancelKey?: string
  
  /**
   * 请求重试次数
   * @default 0
   * @description 
   * 1. 当请求失败时自动重试的次数
   * 2. 通常用于处理不稳定的网络请求
   */
  retryCount?: number
}

/**
 * 声明合并 - 扩展 axios 模块的类型定义
 */
declare module 'axios' {
  /**
   * 扩展 AxiosInstance 接口
   */
  interface AxiosInstance {
    /**
     * 根据 cancelKey 取消请求
     * @param key 要取消的请求key
     * @description
     * 1. 会取消所有配置了相同cancelKey的pending请求
     * 2. 典型场景:路由切换时取消所有非必要请求
     */
    cancelByKey(key: string): void
  }
}

6.3 监控上报体系

// 性能监控埋点
/**
 * 上报被取消的请求信息
 * @param {Object} config - axios请求配置对象
 * @description 
 * 用于收集和分析被主动取消的请求数据
 */
const reportCancelledRequest = (config) => {
  // 计算请求持续时间(从开始到被取消)
  const duration = Date.now() - config.metadata.startTime
  
  // 发送取消分析数据
  analytics.send({
    type: 'REQUEST_CANCELED',  // 事件类型标识
    url: config.url,           // 请求URL
    duration,                  // 请求持续时间(ms)
    cancelReason: config.signal?.reason  // 取消原因(来自AbortSignal)
  })
}

/**
 * 请求拦截器 - 添加元数据
 * @description 
 * 在请求开始时记录时间戳,用于后续计算持续时间
 */
axios.interceptors.request.use(config => {
  // 在config上添加metadata对象,记录请求开始时间
  config.metadata = { 
    startTime: Date.now()  // 使用高精度时间戳
  }
  return config
})

/**
 * 响应拦截器(错误处理) - 监控取消的请求
 * @description 
 * 专门处理请求被取消的情况
 */
axios.interceptors.response.use(
  null,  // 跳过成功响应
  error => {
    // 检查是否是取消错误
    if (axios.isCancel(error)) {
      // 上报取消的请求信息
      reportCancelledRequest(error.config)
    }
    // 继续传递错误
    return Promise.reject(error)
  }
)

七、总结与展望

7.1 最佳实践清单

  1. 组件级
    • 使用watch自动取消旧请求
    • 组合onUnmounted进行资源清理
  2. 应用级
    • 路由切换全局取消请求
    • 实现请求优先级队列
  3. 架构级
    • 建立统一的请求管理中心
    • 实现全链路监控体系

7.2 未来演进方向

  1. Web Streams整合:支持数据流中断
  2. Web Workers支持:后台请求管理
  3. HTTP/3 QUIC协议:原生取消支持
  4. AI预测取消:基于用户行为预判

通过合理运用请求取消机制,开发者可以构建出更健壮、更高效的Web应用。在Vue3生态中,结合组合式API和响应式系统的特性,能够实现更优雅的请求管理方案。建议根据项目规模选择合适的实现策略,小型项目可采用组件级管理,中大型项目推荐使用集中式请求管理方案。