浅析什么是AbortController API、基础用法、常见使用场景、使用最佳实践、注意事项及陷阱

一、什么是 AbortController?

  通俗理解: 就像给异步操作装了一个"紧急停止按钮"。想象你在网上点外卖:

  • 下单后发现地址填错了,想取消 → 这就是 abort
  • 如果外卖已经送出,取消就失败了 → 请求已完成,abort 无效

二、基础用法

1、最简单的例子

// 创建控制器
const controller = new AbortController()
// 发起请求,传入 signal
fetch('/api/data', {
  signal: controller.signal
})
// 3 秒后取消请求
setTimeout(() => {
  controller.abort()
}, 3000)

2、核心概念

const controller = new AbortController()
// ① signal: 信号对象(传给请求)
const signal = controller.signal
// ② abort(): 取消方法(主动调用)
controller.abort()
// ③ aborted: 是否已取消(状态查询)
console.log(signal.aborted) // true/false
// ④ 监听取消事件
signal.addEventListener('abort', () => {
  console.log('请求被取消了')
})

三、常见使用场景

  场景 1:搜索防抖(最常用)

let controller: AbortController | null = null
const handleSearch = async (keyword: string) => {
  // 取消上一次搜索
  controller?.abort()
  // 创建新的控制器
  controller = new AbortController()
  try {
    const response = await fetch(`/api/search?q=${keyword}`, {
      signal: controller.signal
    })
    const data = await response.json()
    displayResults(data)
  } catch (error) {
    // 忽略取消错误
    if (error.name === 'AbortError') {
      console.log('搜索已取消')
      return
    }
    // 处理真正的错误
    console.error('搜索失败:', error)
  }
}
// 用户快速输入:a → ab → abc
// 只有最后一次 "abc" 的请求会生效

  场景 2:超时控制

const fetchWithTimeout = async (url: string, timeout: number = 5000) => {
  const controller = new AbortController()
  // 设置超时
  const timeoutId = setTimeout(() => {
    controller.abort()
  }, timeout)
  try {
    const response = await fetch(url, {
      signal: controller.signal
    })
    clearTimeout(timeoutId) // 成功后清除定时器
    return await response.json()
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('请求超时')
    }
    throw error
  }
}
// 使用
try {
  const data = await fetchWithTimeout('/api/slow', 3000)
} catch (error) {
  console.error(error.message) // "请求超时"
}

  场景 3:组件卸载时取消请求

<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue'
const data = ref([])
let controller: AbortController | null = null
const loadData = async () => {
  controller = new AbortController()
  try {
    const response = await fetch('/api/data', {
      signal: controller.signal
    })
    data.value = await response.json()
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error(error)
    }
  }
}
// 组件卸载时取消请求
onBeforeUnmount(() => {
  controller?.abort()
})
loadData()
</script>

  场景 4:多个请求共享一个 controller

const controller = new AbortController()
// 同时发起多个请求
Promise.all([
  fetch('/api/user', { signal: controller.signal }),
  fetch('/api/posts', { signal: controller.signal }),
  fetch('/api/comments', { signal: controller.signal })
])
// 一次性取消所有请求
controller.abort()

  场景 5:手动取消按钮

<template>
  <div>
    <button @click="loadData" :disabled="loading">加载数据</button>
    <button @click="cancelRequest" :disabled="!loading">取消</button>
    <p v-if="loading">加载中...</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const loading = ref(false)
let controller: AbortController | null = null

const loadData = async () => {
  controller = new AbortController()
  loading.value = true
  
  try {
    const response = await fetch('/api/large-data', {
      signal: controller.signal
    })
    const data = await response.json()
    console.log(data)
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('用户取消了加载')
    }
  } finally {
    loading.value = false
  }
}

const cancelRequest = () => {
  controller?.abort()
}
</script>

四、最佳实践

✅ 1. 总是检查 AbortError

try {
  await fetch(url, { signal })
} catch (error) {
  if (error.name === 'AbortError') {
    // 这是正常的取消,不是错误
    console.log('请求已取消')
    return
  }
  // 处理真正的错误
  handleError(error)
}

✅ 2. 封装可复用的 Hook(Vue)

// composables/useAbortableFetch.ts
import { ref, onBeforeUnmount } from 'vue'
export function useAbortableFetch<T>() {
  const loading = ref(false)
  const error = ref<Error | null>(null)
  const data = ref<T | null>(null)
  let controller: AbortController | null = null

  const execute = async (url: string, options = {}) => {
    // 取消上一次请求
    controller?.abort()
    controller = new AbortController()
    
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal
      })
      data.value = await response.json()
    } catch (err) {
      if (err.name !== 'AbortError') {
        error.value = err
      }
    } finally {
      loading.value = false
    }
  }

  const abort = () => {
    controller?.abort()
  }

  // 组件卸载时自动取消
  onBeforeUnmount(() => {
    controller?.abort()
  })

  return { data, loading, error, execute, abort }
}

// 使用
const { data, loading, execute, abort } = useAbortableFetch()
await execute('/api/users')

✅ 3. 配合防抖使用

import { debounce } from 'lodash-es'

let controller: AbortController | null = null

const searchAPI = async (keyword: string) => {
  controller?.abort()
  controller = new AbortController()
  
  const response = await fetch(`/api/search?q=${keyword}`, {
    signal: controller.signal
  })
  return response.json()
}

// 防抖 + 取消旧请求
const handleSearch = debounce(async (keyword: string) => {
  try {
    const results = await searchAPI(keyword)
    displayResults(results)
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error(error)
    }
  }
}, 300)

✅ 4. 添加取消原因(Chrome 98+)

const controller = new AbortController()
// 可以传递取消原因
controller.abort('用户主动取消')
// 或传递错误对象
controller.abort(new Error('超时'))
// 捕获时可以获取原因
try {
  await fetch(url, { signal: controller.signal })
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('取消原因:', controller.signal.reason)
  }
}

五、注意事项和陷阱

❌ 陷阱 1:重复使用 controller

// ❌ 错误:controller 只能使用一次
const controller = new AbortController()
controller.abort()

fetch('/api/1', { signal: controller.signal }) // 立即被取消
fetch('/api/2', { signal: controller.signal }) // 也会立即被取消

// ✅ 正确:每次都创建新的
let controller = new AbortController()
fetch('/api/1', { signal: controller.signal })

controller = new AbortController() // 创建新的
fetch('/api/2', { signal: controller.signal })

❌ 陷阱 2:忘记处理 AbortError

// ❌ 错误:会把正常的取消当作错误处理
try {
  await fetch(url, { signal })
} catch (error) {
  showErrorToast('请求失败') // 用户取消也会显示错误
}

// ✅ 正确:区分取消和错误
try {
  await fetch(url, { signal })
} catch (error) {
  if (error.name !== 'AbortError') {
    showErrorToast('请求失败')
  }
}

❌ 陷阱 3:忘记清理

// ❌ 可能导致内存泄漏
const controller = new AbortController()
controller.signal.addEventListener('abort', heavyCallback)

// ✅ 清理监听器
const controller = new AbortController()
const handler = () => console.log('aborted')
controller.signal.addEventListener('abort', handler)
// 不再需要时
controller.signal.removeEventListener('abort', handler)

六、兼容性处理

// 检查浏览器支持
if ('AbortController' in window) {
  const controller = new AbortController()
  // 使用 AbortController
} else {
  // 降级方案
  console.warn('浏览器不支持 AbortController')
}

// 或使用 polyfill
// npm install abortcontroller-polyfill
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'

七、总结速查表

场景代码模式
取消搜索 controller?.abort() 后创建新 controller
超时控制 setTimeout(() => controller.abort(), 5000)
组件卸载 onBeforeUnmount(() => controller?.abort())
手动取消 按钮绑定 controller.abort()
批量取消 多个请求共享同一个 signal
错误处理 if (error.name !== 'AbortError')

  核心原则:一次性使用,用完即弃,每次新请求都创建新的 AbortController。

posted @ 2017-12-13 10:35  古兰精  阅读(903)  评论(0)    收藏  举报