发出异步请求后,如何中止请求?🤔

31 阅读4分钟

引言:一个常见的开发痛点

想象一下这个场景:用户在搜索框中输入"前端开发",在请求发出后突然改为"前端框架",此时第一个请求可能比第二个请求更晚返回,导致显示错误的结果。或者更糟:用户快速切换页面,但上一页的请求仍在后台尝试更新已卸载的组件... 这些问题都指向同一个核心需求:我们需要能够取消不再需要的异步操作。而 AbortController就是浏览器原生提供的解决方案。

第一部分:什么是 AbortController?从概念说起

1.1 简单的比喻

把 AbortController 想象成一个带遥控器的烟花发射器

  • new AbortController()- 你拿到了遥控器
  • controller.signal- 遥控器与烟花的信号连接
  • controller.abort()- 按下遥控器的"中止"按钮
  • 请求被取消 - 烟花发射被中止

1.2 技术定义

AbortController是一个 JavaScript 内置对象,它提供了一种方式来中止一个或多个 Web 请求。主要包括两个部分:

  • AbortController实例:用于控制中止
  • AbortSignal对象:用于传递中止信号
// 创建控制器实例
const controller = new AbortController();
// 获取信号对象
const signal = controller.signal;

// 使用信号
fetch('/api/data', { signal });

// 中止请求
controller.abort();

第二部分:为什么需要 AbortController?

2.1 优化性能,避免不必要的网络请求

用户快速操作时的请求瀑布:

输入"a" --> 请求A发出
输入"ab" --> 请求B发出(但请求A仍在进行)
输入"abc" --> 请求C发出(但请求AB仍在浪费资源)

2.2 提升用户体验

  • 搜索框的智能防抖
  • 页面导航时的请求清理
  • 大型文件上传的中止能力

第三部分:核心特性与工作原理

3.1 一次性特性:最重要的使用原则

AbortController 一旦中止,就不能再次使用。这是理解其用法的关键。

// ❌ 错误用法:尝试重复使用
const controller = new AbortController();

// 第一次使用
fetch('/api/1', { signal: controller.signal });
controller.abort(); // 中止第一个请求

// 第二次使用同一个控制器 - 会立即失败!
fetch('/api/2', { signal: controller.signal });

// ✅ 正确做法:每次需要时创建新实例
function makeCancelableRequest(url) {
  const controller = new AbortController(); // 每次都是新的!
  const signal = controller.signal;
  
  fetch(url, { signal })
    .then(response => response.json())
    .catch(error => {
      if (error.name === 'AbortError') {
        console.log('请求被主动取消');
      }
    });
  
  return controller;
}

3.2 信号传播机制

一个 AbortController 可以控制多个请求:

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

// 同时发起多个请求,使用同一个信号
const request1 = fetch('/api/users', { signal });
const request2 = fetch('/api/posts', { signal });
const request3 = fetch('/api/comments', { signal });

// 一次中止,全部取消
controller.abort(); // 所有三个请求都会被中止

第四部分:实战应用场景

4.1 基础用法:组件卸载时取消请求

Vue 3 组合式 API 示例

<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else>
      <div v-for="user in users" :key="user.id">
        {{ user.name }}
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    const users = ref([]);
    const loading = ref(true);
    let controller = null;

    onMounted(async () => {
      controller = new AbortController();

      try {
        const response = await fetch('/api/users', {
          signal: controller.signal
        });
        users.value = await response.json();
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('获取用户列表失败:', error);
        }
      } finally {
        loading.value = false;
      }
    });

    onUnmounted(() => {
      if (controller) {
        controller.abort();
      }
    });

    return { users, loading };
  }
};
</script>

4.2 高级用法:智能搜索与防抖

这是 AbortController 最强大的应用场景之一:

import { useState, useEffect, useRef } from 'react';

function SmartSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [searching, setSearching] = useState(false);
  
  // 使用 ref 保存最新的控制器
  const controllerRef = useRef(null);

  useEffect(() => {
    // 清除搜索结果的函数
    const clearSearch = () => {
      setResults([]);
      setSearching(false);
    };

    // 如果搜索词为空,清空结果
    if (!query.trim()) {
      clearSearch();
      return;
    }

    // 防抖函数
    const timeoutId = setTimeout(async () => {
      // 如果已有请求在进行,先取消它
      if (controllerRef.current) {
        controllerRef.current.abort();
      }

      // 创建新的控制器
      controllerRef.current = new AbortController();
      setSearching(true);

      try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
          signal: controllerRef.current.signal
        });
        
        if (!response.ok) throw new Error('搜索失败');
        
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('搜索错误:', error);
          setResults([]);
        }
      } finally {
        setSearching(false);
      }
    }, 300); // 300ms 防抖

    // 清理函数
    return () => {
      clearTimeout(timeoutId);
    };
  }, [query]); // 依赖 query,query 变化时重新执行

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="输入搜索关键词..."
        className="search-input"
      />
      
      {searching && <div className="searching">搜索中...</div>}
      
      <div className="results">
        {results.map((result, index) => (
          <div key={index} className="result-item">
            {result.title}
          </div>
        ))}
      </div>
    </div>
  );
}

4.3 文件上传的中止控制

function FileUploader() {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  const controllerRef = useRef(null);

  const uploadFile = async (file) => {
    if (controllerRef.current) {
      controllerRef.current.abort(); // 取消可能存在的上传
    }

    controllerRef.current = new AbortController();
    setUploading(true);
    setProgress(0);

    try {
      const formData = new FormData();
      formData.append('file', file);

      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
        signal: controllerRef.current.signal,
      });

      if (!response.ok) throw new Error('上传失败');
      
      const result = await response.json();
      console.log('上传成功:', result);
      
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('上传错误:', error);
      }
    } finally {
      setUploading(false);
      setProgress(0);
    }
  };

  const cancelUpload = () => {
    if (controllerRef.current) {
      controllerRef.current.abort();
    }
  };

  return (
    <div>
      <input
        type="file"
        onChange={(e) => uploadFile(e.target.files[0])}
        disabled={uploading}
      />
      {uploading && (
        <div>
          <progress value={progress} max="100" />
          <button onClick={cancelUpload}>取消</button>
        </div>
      )}
    </div>
  );
}

第五部分:深入理解与最佳实践

5.1 错误处理的艺术

正确的错误处理是使用 AbortController 的关键:

async function makeRequest(url, options = {}) {
  const controller = new AbortController();
  const signal = controller.signal;

  try {
    const response = await fetch(url, { ...options, signal });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return await response.json();
    
  } catch (error) {
    // 关键:区分不同类型的错误
    if (error.name === 'AbortError') {
      console.log('请求被用户取消');
      // 不需要向用户显示错误消息
      return null;
    } else if (error.name === 'TypeError' && error.message.includes('fetch')) {
      console.error('网络错误或 CORS 问题:', error);
      throw new Error('网络连接失败,请检查网络设置');
    } else {
      console.error('请求失败:', error);
      throw error; // 重新抛出其他错误
    }
  }
}

5.2 与不同技术栈的集成

Axios 集成

// 方法1:使用 AbortController(推荐)
const controller = new AbortController();

axios.get('/api/data', {
  signal: controller.signal
}).then(response => {
  console.log(response.data);
}).catch(error => {
  if (axios.isCancel(error)) {
    console.log('请求被取消');
  }
});

controller.abort();

// 方法2:使用传统的 CancelToken(旧版本)
const source = axios.CancelToken.source();

axios.get('/api/data', {
  cancelToken: source.token
}).catch(error => {
  if (axios.isCancel(error)) {
    console.log('请求被取消');
  }
});

source.cancel('操作被用户取消');

第六部分:常见问题与解决方案

6.1 为什么我的请求还是完成了?

如果调用 abort()时请求已经完成,中止操作不会产生任何效果:

const controller = new AbortController();

fetch('/api/instant-data', { signal: controller.signal })
  .then(response => {
    // 如果请求在这个时间点之前已经完成
    // abort() 调用将不会影响
    return response.json();
  });

// 如果请求在 100ms 内完成,abort() 不会起作用
setTimeout(() => {
  controller.abort();
}, 100);

总结

AbortController 是一个简单但强大的工具,它解决了前端开发中的几个关键问题:

  1. 提升性能- 取消不必要的网络请求
  2. 改善用户体验- 实现更智能的交互

核心使用原则

  • 每次需要时创建新实例(一次性使用)
  • 正确区分中止错误和其他错误
  • 在适当的时机进行清理

适用场景

  • 搜索和筛选功能
  • 文件上传/下载
  • 任何可能需要取消的异步操作