Skip to content

深入理解请求取消机制:从原生到框架实现 ​

1.为什么需要请求取消? ​

在前端开发中,经常会遇到需要取消请求的场景:

  • 用户快速切换页面或标签
  • 搜索框实时搜索时,需要取消上一次未完成的请求
  • 防止重复提交
  • 优化性能,避免无效请求占用资源

2.原生实现方案 ​

2.1 XMLHttpRequest ​

早期使用 XMLHttpRequest 时,可以通过 abort() 方法取消请求:

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

// 取消请求
xhr.abort();

2.2 Fetch API ​

现代浏览器提供了 AbortController API:

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

fetch('api/data', { signal })
  .then(response => response.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求已取消');
    }
  });

// 取消请求
controller.abort();

3.Axios 的取消机制 ​

Axios 提供了两种取消请求的方式:

3.1 CancelToken(已废弃) ​

js
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('api/data', {
  cancelToken: source.token
});

// 取消请求
source.cancel('请求已取消');

3.2 AbortController(推荐) ​

js
const controller = new AbortController();

axios.get('api/data', {
  signal: controller.signal
});

// 取消请求
controller.abort();

4. 实际应用场景 ​

4.1 搜索防抖 ​

js
const SearchComponent = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const controller = useRef(null);

  useEffect(() => {
    return () => {
      if (controller.current) {
        controller.current.abort();
      }
    };
  }, []);

  const handleSearch = async (value) => {
    if (controller.current) {
      controller.current.abort();
    }
    
    controller.current = new AbortController();
    
    try {
      const result = await axios.get(`/api/search?q=${value}`, {
        signal: controller.current.signal
      });
      // 处理结果
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log('搜索请求已取消');
      }
    }
  };
  
  return (
    <input 
      value={searchTerm}
      onChange={e => {
        setSearchTerm(e.target.value);
        handleSearch(e.target.value);
      }}
    />
  );
};

4.2 路由切换取消 ​

js
const useRequestCancel = () => {
  const controllerRef = useRef(null);

  useEffect(() => {
    return () => {
      if (controllerRef.current) {
        controllerRef.current.abort();
      }
    };
  }, []);

  const request = async (url) => {
    if (controllerRef.current) {
      controllerRef.current.abort();
    }
    
    controllerRef.current = new AbortController();
    
    return axios.get(url, {
      signal: controllerRef.current.signal
    });
  };

  return { request };
};

4.3 自定义控制取消 ​

手动实现取消请求,比如说对于cardsType发生变化的时候可以手动取消

js
// net.js
export let cancelRequest;
export const getCards = ({ type, page_num, task_type, needCancel = true }) => {
        let cancelRequest;
        if (needCancel) {
            cancelRequest = axios.CancelToken.source();
        }
        try {
            return axiosClient.post(
                `${BASE_URL}/task/cards`,
                { type, page_num, task_type },
                {
                    cancelToken: needCancel ? cancelRequest.token : undefined,
                }
            )
                .catch((error) => {
                    if (axios.isCancel(error)) {
                        console.log('请求已取消');
                    } else {
                        console.error(error);
                    }
                });
        } catch (error) {
            console.error(error);
        }
    };

// 组件中
  useDebounceEffect(() => {
    // 在切换 cardsType 时主动取消上一次请求
    if (cancelRequest) {
      cancelRequest.cancel('CardType changed');
    }
    // ... existing code ...
  }, [cardsType], 100)

5. 请求取消的本质与限制 ​

5.1 前端取消请求的本质 ​

需要明确一个重要概念:前端的请求取消实际上并不能真正"取消"已经发出的请求。当我们调用取消方法时:

  • 如果请求还未发出,则可以真正地取消
  • 如果请求已经发出:
    • 请求仍会到达服务器并执行相应的操作
    • 只是客户端不再等待响应,也不会执行后续的处理逻辑

5.2 典型场景分析 ​

js
// 以添加用户为例
const addUser = async (userData) => {
  const controller = new AbortController();
  
  try {
    await axios.post('/api/users', userData, {
      signal: controller.signal
    });
    console.log('用户添加成功');
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('请求已取消');
      return;
    }
    console.error('添加用户失败:', error);
  }
  
  // 取消请求
  controller.abort();
};

在上面的例子中:

  • 如果在请求发出前取消,则请求不会发送到服务器
  • 如果在请求已发出后取消:
    • 服务器依然会处理添加用户的操作
    • 客户端不会执行后续的 console.log('用户添加成功')

5.3 最佳实践建议 ​

  1. 对于写操作(POST/PUT/DELETE):

    • 应谨慎使用取消机制
    • 建议实现服务端幂等性来处理可能的重复请求
    • 考虑添加事务回滚机制
  2. 对于读操作(GET):

    • 可以放心使用取消机制
    • 适用于搜索、列表加载等场景
    • 有助于减少无效请求和资源占用
  3. 关键业务场景:

    • 建议添加请求状态追踪
    • 实现完整的错误处理机制
    • 考虑添加用户提示,告知操作可能已经执行