axios取消错误:CanceledError取消异常
在使用axios进行异步HTTP请求时,取消请求是一个常见需求,比如用户导航离开当前页面或发起新的请求时需要取消之前的请求。此时,axios会抛出CanceledError取消异常。本文将深入探讨CanceledError的产生机制、处理方法及最佳实践,帮助开发者更好地理解和应对这一常见错误。
CanceledError的定义与源码解析
CanceledError是axios中用于表示请求被取消的错误类型,它继承自AxiosError,并添加了特定的标识属性。
源码定义
CanceledError的源码位于lib/cancel/CanceledError.js:
import AxiosError from '../core/AxiosError.js';
import utils from '../utils.js';
/**
* A `CanceledError` is an object that is thrown when an operation is canceled.
*
* @param {string=} message The message.
* @param {Object=} config The config.
* @param {Object=} request The request.
*
* @returns {CanceledError} The created error.
*/
function CanceledError(message, config, request) {
// eslint-disable-next-line no-eq-null,eqeqeq
AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request);
this.name = 'CanceledError';
}
utils.inherits(CanceledError, AxiosError, {
__CANCEL__: true
});
export default CanceledError;
从源码可以看出,CanceledError具有以下特点:
- 继承自AxiosError类
- 错误代码为AxiosError.ERR_CANCELED
- 名称为'CanceledError'
- 具有__CANCEL__: true的标识属性,用于判断错误类型
错误判断工具
axios提供了一个工具函数isCancel来判断一个错误是否为取消错误,源码位于lib/cancel/isCancel.js:
export default function isCancel(value) {
return !!(value && value.__CANCEL__);
}
这个简单的函数通过检查对象是否具有__CANCEL__属性来判断是否为取消错误。
取消机制详解
axios提供了两种主要方式来取消请求并触发CanceledError:CancelToken和AbortController。
CancelToken方式
CancelToken是axios早期版本引入的取消机制,通过创建取消令牌来实现请求取消。其源码位于lib/cancel/CancelToken.js。
基本用法
// 创建取消令牌
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 发起请求时关联取消令牌
axios.get('/api/data', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('请求已取消:', thrown.message);
} else {
// 处理其他错误
}
});
// 取消请求(参数为可选的取消消息)
source.cancel('用户取消了请求');
源码解析
CancelToken的核心实现是基于Promise:
class CancelToken {
constructor(executor) {
// ...省略部分代码...
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
// ...省略部分代码...
executor(function cancel(message, config, request) {
if (token.reason) {
// 已经取消过了
return;
}
token.reason = new CanceledError(message, config, request);
resolvePromise(token.reason);
});
}
// ...省略其他方法...
/**
* 静态方法,返回一个包含token和cancel函数的对象
*/
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token,
cancel
};
}
}
当调用cancel方法时,会创建一个CanceledError实例并通过resolvePromise触发promise的解决,从而取消请求并抛出CanceledError。
AbortController方式
AbortController是现代浏览器提供的标准API,用于取消一个或多个DOM请求。axios从0.22.0版本开始支持AbortController。
基本用法
// 创建AbortController实例
const controller = new AbortController();
// 发起请求时关联signal
axios.get('/api/data', {
signal: controller.signal
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('请求已取消:', thrown.message);
} else {
// 处理其他错误
}
});
// 取消请求
controller.abort();
两种取消方式对比
| 特性 | CancelToken | AbortController |
|---|---|---|
| 标准性 | axios特有,非标准 | 浏览器标准API |
| 创建方式 | CancelToken.source() | new AbortController() |
| 取消触发 | source.cancel() | controller.abort() |
| 信号传递 | 通过token属性 | 通过signal属性 |
| 取消消息 | 支持自定义消息 | 不直接支持,需额外实现 |
| 多请求取消 | 需多个token | 可同一signal关联多个请求 |
| 兼容性 | 所有axios版本 | 现代浏览器和Node.js 15+ |
| 推荐程度 | 不推荐使用(已过时) | 推荐使用(标准方案) |
CancelToken转AbortSignal
为了平滑过渡到标准API,axios在CancelToken上提供了toAbortSignal()方法:
const source = CancelToken.source();
const signal = source.token.toAbortSignal();
// 可以将signal用于其他支持AbortSignal的API
fetch('/api/data', { signal })
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
}
});
// 通过CancelToken的cancel方法取消请求
source.cancel();
CanceledError触发流程
当请求被取消时,CanceledError的触发流程如下:
从源码角度看,在lib/core/dispatchRequest.js中定义了请求调度的核心逻辑:
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
if (config.signal && config.signal.aborted) {
throw new CanceledError(null, config);
}
}
export default function dispatchRequest(config) {
throwIfCancellationRequested(config);
// ...省略其他代码...
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// ...处理响应...
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// ...处理错误...
}
return Promise.reject(reason);
});
}
throwIfCancellationRequested函数会在请求发起前和响应处理前检查取消状态,如果已取消则抛出CanceledError。
错误捕获与处理
正确捕获和处理CanceledError是确保应用健壮性的重要部分。
基本错误捕获
// 使用try/catch捕获取消错误
async function fetchData() {
const controller = new AbortController();
try {
const response = await axios.get('/api/data', {
signal: controller.signal
});
return response.data;
} catch (error) {
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message);
// 可以在这里进行取消后的清理工作
return null; // 或其他合适的默认值
}
// 处理其他类型的错误
console.error('请求失败:', error.message);
throw error; // 重新抛出非取消错误
}
}
在拦截器中处理取消错误
可以在axios拦截器中统一处理CanceledError:
// 请求拦截器
axios.interceptors.request.use(
config => {
// 在请求发送前的处理
return config;
},
error => {
if (axios.isCancel(error)) {
console.log('请求拦截器捕获到取消错误:', error.message);
}
return Promise.reject(error);
}
);
// 响应拦截器
axios.interceptors.response.use(
response => response,
error => {
if (axios.isCancel(error)) {
console.log('响应拦截器捕获到取消错误:', error.message);
// 可以在这里统一处理取消后的逻辑
}
return Promise.reject(error);
}
);
在React组件中处理取消错误
在React组件中使用axios时,通常需要在组件卸载时取消未完成的请求:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function DataFetchingComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await axios.get('/api/data', {
signal: controller.signal
});
setData(response.data);
} catch (error) {
if (!axios.isCancel(error)) {
console.error('请求失败:', error);
}
}
};
fetchData();
// 组件卸载时取消请求
return () => {
controller.abort();
};
}, []);
return (
<div>
{data ? <div>{JSON.stringify(data)}</div> : <div>加载中...</div>}
</div>
);
}
实际应用场景与示例
1. 用户操作取消
实现一个搜索框,当用户输入新内容时取消之前的搜索请求:
// HTML: <input type="text" id="search-input" />
const input = document.getElementById('search-input');
let controller = null;
input.addEventListener('input', debounce(async (e) => {
// 如果有正在进行的请求,取消它
if (controller) {
controller.abort();
}
// 创建新的AbortController
controller = new AbortController();
try {
const response = await axios.get(`/api/search?q=${e.target.value}`, {
signal: controller.signal
});
// 处理搜索结果
renderResults(response.data);
} catch (error) {
if (!axios.isCancel(error)) {
console.error('搜索失败:', error);
showError('搜索失败,请重试');
}
} finally {
// 如果请求完成或取消,重置controller
if (controller.signal.aborted) {
controller = null;
}
}
}, 300));
// 简单的防抖函数实现
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
2. 超时自动取消
结合axios的timeout配置实现超时自动取消:
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
// 设置超时取消
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await axios({
url,
...options,
signal
});
clearTimeout(timeoutId); // 请求成功,清除超时
return response;
} catch (error) {
clearTimeout(timeoutId);
if (axios.isCancel(error)) {
// 判断是用户取消还是超时取消
const errorMessage = error.message || '请求超时';
throw new Error(`请求已取消: ${errorMessage}`);
}
throw error;
}
}
// 使用示例
fetchWithTimeout('/api/data', { method: 'GET' }, 3000)
.then(response => console.log(response.data))
.catch(error => console.error(error.message));
3. 取消多个并发请求
同时取消多个并发请求:
// 使用AbortController取消多个请求
async function fetchMultipleResources() {
const controller = new AbortController();
const { signal } = controller;
try {
// 多个请求共享同一个signal
const requests = [
axios.get('/api/data1', { signal }),
axios.get('/api/data2', { signal }),
axios.get('/api/data3', { signal })
];
// 同时取消的按钮点击处理
document.getElementById('cancel-btn').addEventListener('click', () => {
controller.abort();
});
// 等待所有请求完成
const results = await Promise.all(requests);
return results.map(res => res.data);
} catch (error) {
if (axios.isCancel(error)) {
console.log('多个请求已被取消');
// 可以在这里处理取消后的逻辑
return [];
}
console.error('请求出错:', error);
throw error;
}
}
4. React Hooks封装
封装一个带取消功能的自定义Hook:
import { useRef, useState, useCallback } from 'react';
import axios from 'axios';
function useAxiosWithCancel() {
const cancelControllers = useRef(new Map());
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const axiosWithCancel = useCallback(async (config, requestId = 'default') => {
// 取消同一ID的现有请求
if (cancelControllers.current.has(requestId)) {
const controller = cancelControllers.current.get(requestId);
controller.abort();
}
const controller = new AbortController();
cancelControllers.current.set(requestId, controller);
setLoading(true);
setError(null);
try {
const response = await axios({
...config,
signal: controller.signal
});
return response.data;
} catch (err) {
if (!axios.isCancel(err)) {
setError(err);
throw err;
}
// 取消错误不向上抛出,由组件决定是否处理
return null;
} finally {
setLoading(false);
cancelControllers.current.delete(requestId);
}
}, []);
// 取消所有请求的方法
const cancelAllRequests = useCallback(() => {
cancelControllers.current.forEach(controller => {
controller.abort();
});
cancelControllers.current.clear();
}, []);
return { axiosWithCancel, loading, error, cancelAllRequests };
}
// 组件中使用
function DataComponent() {
const { axiosWithCancel, loading, error, cancelAllRequests } = useAxiosWithCancel();
const fetchData = async () => {
try {
const data = await axiosWithCancel({
url: '/api/data',
method: 'GET'
}, 'data-request');
if (data) {
// 处理数据
console.log(data);
}
} catch (err) {
console.error('请求失败:', err);
}
};
useEffect(() => {
return () => {
cancelAllRequests();
};
}, [cancelAllRequests]);
return (
<div>
<button onClick={fetchData} disabled={loading}>
{loading ? '加载中...' : '获取数据'}
</button>
{error && <div className="error">错误: {error.message}</div>}
</div>
);
}
最佳实践与常见问题
避免内存泄漏
取消请求不仅是为了避免无用的网络请求,也是为了防止内存泄漏:
// 不好的实践:未清理的订阅可能导致内存泄漏
function badPractice() {
const source = axios.CancelToken.source();
axios.get('/api/data', { cancelToken: source.token })
.then(response => {
// 处理响应
updateUI(response.data);
});
// 没有在组件卸载或适当时候调用source.cancel()
}
// 好的实践:组件卸载时取消请求
function goodPractice() {
const controller = new AbortController();
axios.get('/api/data', { signal: controller.signal })
.then(response => {
updateUI(response.data);
})
.catch(error => {
if (!axios.isCancel(error)) {
handleError(error);
}
});
// 在组件卸载或不需要结果时调用
return () => {
controller.abort();
};
}
区分不同类型的取消错误
在复杂应用中,可能需要区分不同原因的取消:
// 自定义取消原因枚举
const CancelReason = {
USER_ACTION: 'user_action',
NAVIGATION: 'navigation',
TIMEOUT: 'timeout',
DUPLICATE: 'duplicate'
};
// 扩展取消方法,包含原因
function cancelWithReason(controller, reason, message) {
// 使用AbortSignal的reason属性传递额外信息
controller.abort({
type: 'cancel',
reason,
message: message || '请求已取消'
});
}
// 使用示例
const controller = new AbortController();
axios.get('/api/data', {
signal: controller.signal,
cancelReason: CancelReason.DUPLICATE // 自定义属性传递原因
})
.catch(error => {
if (axios.isCancel(error)) {
// 从signal的reason中获取详细信息
const cancelDetails = controller.signal.reason;
switch (cancelDetails.reason) {
case CancelReason.USER_ACTION:
showNotification('操作已取消');
break;
case CancelReason.TIMEOUT:
showError('请求超时,请重试');
break;
case CancelReason.DUPLICATE:
console.log('重复请求已取消');
break;
default:
showError('请求已取消');
}
}
});
// 取消请求并指定原因
cancelWithReason(controller, CancelReason.DUPLICATE, '检测到重复请求');
常见问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 取消后仍收到响应 | 确保在then/catch中检查请求状态 |
| 重复取消导致错误 | 使用标志位或try/finally确保只取消一次 |
| 内存泄漏 | 在组件卸载或不需要结果时取消请求 |
| 无法区分取消类型 | 自定义取消原因并在错误中传递 |
| 旧代码兼容性 | 使用CancelToken.toAbortSignal()桥接 |
总结
CanceledError是axios中处理请求取消的核心机制,通过本文的深入解析,我们了解了:
- CanceledError的源码实现与基本特性
- 两种取消请求的方式:CancelToken(已过时)和AbortController(推荐)
- CanceledError的触发流程与捕获处理方法
- 多种实际应用场景与代码示例
- 最佳实践与常见问题解决方案
在实际开发中,推荐使用标准的AbortController API来处理请求取消,它不仅是浏览器标准,还能与其他支持AbortSignal的API(如fetch、Streams等)兼容。同时,良好的取消错误处理机制能够提升应用的健壮性和用户体验,特别是在用户频繁操作或网络不稳定的情况下。
正确使用和处理CanceledError,能够让我们的应用更加健壮、响应更快,同时避免不必要的资源浪费和潜在的内存泄漏问题。
参考资料
- axios官方文档: README.md
- CanceledError源码: lib/cancel/CanceledError.js
- CancelToken源码: lib/cancel/CancelToken.js
- isCancel工具函数: lib/cancel/isCancel.js
- 请求调度逻辑: lib/core/dispatchRequest.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



