JavaScript:AbortController API 以及Promise.race优化
AbortController 完全指南
1. 什么是 AbortController?
1.1 定义与定位
AbortController 是 浏览器与 Node.js 原生提供的异步操作控制接口,属于 ES2017 标准,核心作用是通过“信号传递机制”实现异步任务的协作式取消——即主动通知已启动的异步任务“停止执行并清理资源”,而非强制终止(JavaScript 无抢占式终止能力)。
它并非独立功能,而是为解决“异步任务失控”问题而生的标准化方案,覆盖 fetch、定时器、事件监听、Promise 链等几乎所有异步场景。
1.2 核心组成与 API
AbortController 由“控制器实例”和“信号对象”两部分组成,所有操作围绕这两个核心展开。
1.2.1 核心 API 表格
| 成员/方法 | 类型 | 作用描述 |
|---|---|---|
new AbortController() |
构造函数 | 创建 AbortController 实例,内部自动生成关联的 AbortSignal 对象 |
controller.signal |
属性 | 返回 AbortSignal 信号对象,用于绑定到异步任务,传递“取消指令” |
controller.abort() |
方法 | 触发取消:将 signal.aborted 设为 true,同步触发所有 signal 的 abort 事件 |
signal.aborted |
属性 | 布尔值(只读),true 表示已取消,false 表示未取消,用于任务内部判断 |
signal.addEventListener('abort', cb) |
方法 | 为信号绑定“取消回调”,任务通过该回调实现资源清理 |
1.2.2 基础使用示例(最小demo)
// 1. 创建控制器实例
const controller = new AbortController();
// 2. 获取信号对象
const { signal } = controller;
// 3. 监听信号的“取消事件”(模拟异步任务的资源清理)
signal.addEventListener('abort', () => {
console.log('收到取消指令,开始清理资源');
});
// 4. 触发取消(模拟需要终止任务的场景,如用户点击“取消按钮”)
controller.abort();
// 5. 检查信号状态
console.log(signal.aborted); // 输出: true
2. AbortController 核心能力与应用场景
AbortController 的核心价值是“让异步任务可控”,以下是 4 个高频实战场景,均包含完整代码与细节注释。
2.1 场景1:取消网络请求(最常用)
fetch API 原生支持 signal 参数,可直接绑定 AbortController 信号,实现“请求发送后随时取消”(如用户跳转页面、点击取消按钮)。
完整代码示例
// 1. 创建控制器(通常在请求发起时创建)
const requestController = new AbortController();
const { signal } = requestController;
// 2. 发起带取消能力的 fetch 请求
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/user/list', {
method: 'GET',
signal: signal, // 关键:将信号绑定到请求
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
console.log('请求成功:', data);
return data;
} catch (err) {
// 关键:区分“取消错误”与“业务错误”
if (err.name === 'AbortError') {
console.log('请求已被主动取消(非错误)');
return null; // 取消不属于错误,无需抛出
}
// 处理真实业务错误(如网络异常、404、500)
console.error('请求失败(业务错误):', err.message);
throw err;
}
};
// 3. 启动请求
fetchData();
// 4. 模拟“用户点击取消”(3秒后取消请求)
setTimeout(() => {
// 触发取消:请求会立即终止,不会等待服务器响应
requestController.abort();
}, 3000);
关键细节
- 取消时机:
abort()调用后,fetch会立即终止请求,不会等待服务器返回,节省带宽; - 错误区分:取消时抛出的
AbortError需单独处理,避免与“网络超时”“401 未授权”等业务错误混淆; - 重复请求:若同一接口可能多次发起,可通过
signal.aborted判断是否已有未完成请求,避免重复发送。
2.2 场景2:取消定时器/延时任务
setTimeout/setInterval 启动后,需通过 clearTimeout(timerId) 终止,但 timerId 仅在任务内部可知。借助 AbortController,可在外部触发取消时通知任务内部执行 clear。
完整代码示例(封装可取消定时器)
/**
* 封装可取消的定时器
* @param {number} delay - 延迟时间(毫秒)
* @param {any} value - 定时器完成后 resolve 的值
* @returns {Object} { promise: Promise, cancel: Function } - 可取消的 Promise 与取消方法
*/
function createCancelableTimer(delay, value) {
// 1. 为每个定时器创建独立控制器(避免多个定时器共享信号)
const timerController = new AbortController();
const { signal } = timerController;
// 2. 创建 Promise 封装定时器
const timerPromise = new Promise((resolve) => {
// 启动定时器,保存 timerId(用于后续清理)
const timerId = setTimeout(() => {
// 边界判断:若已取消,不执行 resolve
if (signal.aborted) {
console.log(`定时器(${delay}ms):已取消,不执行 resolve`);
return;
}
console.log(`定时器(${delay}ms):正常执行`);
resolve(value);
}, delay);
// 3. 监听取消信号:收到指令时清理定时器
signal.addEventListener('abort', () => {
clearTimeout(timerId); // 核心:主动清理定时器资源
console.log(`定时器(${delay}ms):已清理 timerId = ${timerId}`);
});
});
// 4. 返回 Promise 与取消方法(外部通过 cancel 触发取消)
return {
promise: timerPromise,
cancel: () => timerController.abort()
};
}
// 5. 使用可取消定时器
const { promise: taskPromise, cancel: cancelTask } = createCancelableTimer(5000, '任务完成');
// 监听定时器结果
taskPromise.then((res) => {
console.log('定时器结果:', res);
});
// 3秒后取消定时器(模拟外部触发取消)
setTimeout(() => {
cancelTask();
}, 3000);
关键细节
- 独立控制器:每个定时器需创建自己的
AbortController,避免一个定时器取消时影响其他定时器; - 边界判断:定时器回调内需检查
signal.aborted,防止“信号触发时定时器已进入任务队列”导致的无效resolve; - 清理时机:
abort回调内必须执行clearTimeout,否则定时器到时间后仍会执行回调。
2.3 场景3:取消 Promise 链
复杂异步流程(如“请求→数据处理→UI 渲染”)中,若中间需要中断(如用户离开页面),可通过 AbortController 让整个链条停止执行,避免无效计算与 UI 错误更新。
完整代码示例
// 1. 创建全局控制器(控制整个 Promise 链)
const flowController = new AbortController();
const { signal } = flowController;
// 步骤1:请求数据
const fetchUser = async () => {
console.log('步骤1:发起用户数据请求');
const response = await fetch('https://api.example.com/user/1', { signal });
return response.json();
};
// 步骤2:处理数据(过滤无效字段)
const processUser = (userData) => {
// 关键:每个步骤都检查是否已取消,避免无效计算
if (signal.aborted) {
throw new Error('流程已取消:数据处理步骤终止');
}
console.log('步骤2:处理用户数据');
return {
id: userData.id,
name: userData.name,
avatar: userData.avatar || '默认头像地址'
};
};
// 步骤3:渲染 UI
const renderUser = (processedData) => {
if (signal.aborted) {
throw new Error('流程已取消:UI 渲染步骤终止');
}
console.log('步骤3:渲染用户 UI', processedData);
// 模拟 DOM 操作
// document.getElementById('user-avatar').src = processedData.avatar;
};
// 串联 Promise 链
const userFlow = async () => {
try {
const rawData = await fetchUser();
const processedData = processUser(rawData);
await renderUser(processedData);
console.log('流程完成');
} catch (err) {
// 区分取消错误与业务错误
if (err.message.includes('流程已取消')) {
console.log('用户流程:已主动中断');
return;
}
console.error('用户流程:业务错误', err);
}
};
// 启动流程
userFlow();
// 2秒后中断流程(模拟用户离开页面)
setTimeout(() => {
flowController.abort();
}, 2000);
关键细节
- 全链检查:每个步骤(请求、处理、渲染)都需判断
signal.aborted,确保链条彻底中断; - 错误抛出自定义信息:便于
catch中区分“取消中断”与“业务错误”; - 异步步骤兼容:若步骤包含异步操作(如
setTimeout),需在异步回调内也添加signal.aborted判断。
2.4 场景4:取消事件监听
页面滚动、窗口大小变化(resize)等事件监听,若在页面销毁时未移除,会导致内存泄漏。借助 AbortController,可一次性取消所有关联的事件监听。
完整代码示例
// 1. 创建控制器(通常在组件挂载时创建)
const eventController = new AbortController();
const { signal } = eventController;
// 2. 绑定带取消能力的事件监听
// 方式:addEventListener 第三个参数传入 { signal }
window.addEventListener('scroll', () => {
console.log('滚动位置:', window.scrollY);
}, { signal }); // 关键:绑定信号
window.addEventListener('resize', () => {
console.log('窗口尺寸:', window.innerWidth + 'x' + window.innerHeight);
}, { signal }); // 同一信号可绑定多个事件
// 3. 组件卸载/页面离开时取消所有监听(避免内存泄漏)
// 模拟 React 组件的 unmount 生命周期
const cleanup = () => {
eventController.abort(); // 一次调用,取消所有绑定该信号的事件监听
console.log('所有事件监听已取消');
};
// 模拟页面离开
setTimeout(() => {
cleanup();
}, 5000);
关键细节
- 信号复用:同一
signal可绑定多个事件监听,abort()调用后所有监听会自动移除,无需逐个调用removeEventListener; - 内存安全:
abort()后,事件监听函数会被释放,避免因“监听函数持有 DOM 引用”导致的内存泄漏; - 兼容性:
addEventListener支持signal参数是 ES2020 特性,低版本浏览器需降级(见 6.2 节)。
3. 为什么需要 AbortController?(不用的3大问题)
在 AbortController 出现前,开发者通常用“自定义 flag”(如 let isCanceled = false)控制异步任务,但存在以下无法解决的问题,而 AbortController 是标准化的解决方案。
3.1 问题1:资源浪费
异步任务启动后,即使不再需要结果,仍会继续执行并占用资源:
- 网络资源:
fetch请求发送后,即使用户跳转页面,请求仍会等待服务器响应,浪费带宽; - 计算资源:
setTimeout(10000)启动后,10秒后回调仍会执行,占用主线程时间片; - 内存资源:未取消的事件监听会持有函数引用,导致垃圾回收(GC)无法回收相关内存。
AbortController 可通过“主动清理”(如 clearTimeout、终止 fetch)释放这些资源。
3.2 问题2:逻辑混乱
“自定义 flag”无法保证任务彻底中断,易导致后续逻辑意外执行:
- 示例:
myPromise.race中,慢任务的定时器到时间后仍会resolve,若后续代码依赖race结果更新 UI,可能导致“重复更新”; - 示例:表单提交后,用户点击“取消”,但
fetch请求仍在继续,成功后会跳转到“提交成功页”,不符合用户预期。
AbortController 可让任务“主动停止执行后续逻辑”(如不调用 resolve、不更新 UI),避免逻辑混乱。
3.3 问题3:多任务管理复杂
若多个异步任务需要“批量取消”,“自定义 flag”需维护多个变量(如 isCanceled1、isCanceled2),逻辑繁琐:
- 示例:页面加载时同时发起 3 个
fetch请求,用户点击“取消加载”时需终止所有请求,用flag需逐个判断,而AbortController可通过一个信号批量取消。
AbortController 的“信号复用”特性,让多任务批量管理变得简单。
4. AbortController 使用指南(标准流程+避坑)
所有场景的使用都遵循“创建控制器→绑定信号→触发取消”的标准三步流程,核心差异仅在“信号绑定”的方式。
4.1 标准三步流程(通用模板)
// 步骤1:创建控制器与信号(1个控制器对应1组需批量取消的任务)
const controller = new AbortController();
const { signal } = controller;
// 步骤2:绑定信号到异步任务(核心,分2种方式)
// 方式A:任务原生支持 signal 参数(如 fetch、addEventListener)
fetch(url, { signal });
window.addEventListener('scroll', callback, { signal });
// 方式B:任务内部监听 signal 的 abort 事件(如 setTimeout、自定义 Promise)
const asyncTask = () => {
// 任务资源(如 timerId、请求对象)
const resource = initResource(); // 如 const timerId = setTimeout(...)
// 监听取消信号,清理资源
signal.addEventListener('abort', () => {
cleanupResource(resource); // 如 clearTimeout(timerId)
});
// 执行任务逻辑
runTaskLogic();
};
// 步骤3:触发取消(在需要终止任务的时机调用)
// 如用户点击“取消按钮”、组件卸载、路由切换
const triggerCancel = () => {
controller.abort();
};
4.2 关键避坑点(高频错误+解决方案)
4.2.1 避坑1:取消后无法恢复信号状态
signal.aborted 是“只读且不可逆”的——abort() 调用后,signal.aborted 会永久变为 true,无法重置为 false。
错误示例:
const controller = new AbortController();
const { signal } = controller;
// 第一次取消
controller.abort();
console.log(signal.aborted); // true
// 尝试重新使用信号(错误:信号已失效)
fetch(url, { signal }); // 会立即抛出 AbortError
解决方案:若需要重新执行任务,必须创建新的 AbortController 实例:
// 封装“可重试”的任务
const createTask = () => {
const newController = new AbortController();
const { signal } = newController;
const taskPromise = fetch(url, { signal });
return { taskPromise, cancel: () => newController.abort() };
};
// 第一次执行
const { taskPromise: task1, cancel: cancel1 } = createTask();
cancel1(); // 取消第一次任务
// 重新执行(创建新控制器)
const { taskPromise: task2, cancel: cancel2 } = createTask(); // 有效
4.2.2 避坑2:未区分“取消错误”与“业务错误”
取消时抛出的 AbortError 不属于“业务错误”,若未单独处理,会导致错误日志混乱或 UI 错误提示。
错误示例:
fetch(url, { signal })
.then(res => res.json())
.catch(err => {
// 错误:将取消错误当作业务错误处理
console.error('请求失败', err);
showErrorToast('请求失败,请重试'); // 错误提示用户
});
解决方案:通过 err.name === 'AbortError' 区分:
fetch(url, { signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消(非错误)');
return; // 不处理为错误
}
// 仅处理业务错误
console.error('请求失败(业务错误)', err);
showErrorToast('请求失败,请重试');
});
4.2.3 避坑3:手动添加的监听器未移除(内存泄漏)
若通过 signal.addEventListener('abort', cb) 手动绑定回调,任务完成后需移除监听器,避免 cb 被信号持有导致内存泄漏。
错误示例:
const timerId = setTimeout(() => {}, 1000);
// 错误:添加监听器后未移除
signal.addEventListener('abort', () => {
clearTimeout(timerId);
});
解决方案:在任务完成或取消后移除监听器:
const timerId = setTimeout(() => {
// 任务正常完成,移除监听器
signal.removeEventListener('abort', abortHandler);
}, 1000);
// 定义可复用的回调函数(便于移除)
const abortHandler = () => {
clearTimeout(timerId);
// 取消后,移除监听器
signal.removeEventListener('abort', abortHandler);
};
signal.addEventListener('abort', abortHandler);
4.2.4 避坑4:多个任务共享控制器导致“误杀”
若多个独立任务共享同一个 AbortController,取消一个任务时会“误杀”其他任务。
错误示例:
const controller = new AbortController();
const { signal } = controller;
// 任务1:5秒后执行
setTimeout(() => console.log('任务1执行'), 5000);
// 任务2:3秒后执行
setTimeout(() => console.log('任务2执行'), 3000);
// 错误:取消任务1时,任务2也被取消
controller.abort();
解决方案:独立任务创建独立控制器,仅将“需批量取消的任务”归为一组:
// 任务1:独立控制器
const controller1 = new AbortController();
setTimeout(() => {
if (!controller1.signal.aborted) console.log('任务1执行');
}, 5000);
// 任务2:独立控制器
const controller2 = new AbortController();
setTimeout(() => {
if (!controller2.signal.aborted) console.log('任务2执行');
}, 3000);
// 仅取消任务1,任务2正常执行
controller1.abort();
5. 结合 myPromise 的实战案例(优化 race 方法)
在自定义 myPromise 的 race 方法中,AbortController 可解决“慢任务不终止”的问题,实现“第一个任务完成后,自动取消所有其他任务”。
5.1 优化前的问题
// 优化前:2秒任务完成后,10秒任务仍会执行 resolve
myPromise.race([
new myPromise(resolve => setTimeout(() => resolve('慢任务'), 10000)),
new myPromise(resolve => setTimeout(() => resolve('快任务'), 2000))
]).then(res => console.log('race结果:', res)); // 输出“快任务”,但10秒后仍会执行“慢任务”的 resolve
5.2 优化后的完整代码(含 AbortController 集成)
class myPromise {
#pending = Symbol("pending");
#fulfilled = Symbol("fulfilled");
#rejected = Symbol("rejected");
#state = this.#pending;
#result = null;
#handler = [];
#abortSignal = null; // 新增:存储取消信号
constructor(func, { signal } = {}) {
// 1. 集成 AbortController 信号
if (signal) {
this.#abortSignal = signal;
// 信号已取消,直接标记为 rejected
if (signal.aborted) {
this.#state = this.#rejected;
this.#result = new Error("Promise aborted");
return;
}
// 监听取消信号,触发 reject
const abortHandler = () => {
if (this.#state === this.#pending) {
this.#state = this.#rejected;
this.#result = new Error("Promise aborted");
this.#Asyncfunction(() => {
this.#handler.forEach(({ onRejected }) => onRejected(this.#result));
this.#handler = [];
});
}
signal.removeEventListener("abort", abortHandler);
};
signal.addEventListener("abort", abortHandler);
}
const resolve = (result) => {
if (this.#state === this.#pending && !this.#abortSignal?.aborted) {
this.#state = this.#fulfilled;
this.#result = result;
this.#Asyncfunction(() => {
this.#handler.forEach(({ onFulfilled }) => onFulfilled(this.#result));
this.#handler = [];
});
}
};
const reject = (result) => {
if (this.#state === this.#pending && !this.#abortSignal?.aborted) {
this.#state = this.#rejected;
this.#result = result;
this.#Asyncfunction(() => {
this.#handler.forEach(({ onRejected }) => onRejected(this.#result));
this.#handler = [];
});
}
};
try {
func(resolve, reject);
} catch (err) {
reject(err);
}
}
// 其他方法(#Asyncfunction、then、catch、finally 等保持不变)...
// 优化后的 race 方法(集成 AbortController)
static race(array) {
return new myPromise((resolve, reject) => {
if (!Array.isArray(array)) {
reject(new TypeError("Argument is not iterable"));
return;
}
// 1. 创建全局控制器,所有 race 任务共享信号
const raceController = new AbortController();
const { signal } = raceController;
let isSettled = false; // 标记是否已有任务完成
array.forEach((item) => {
// 2. 将元素转为 myPromise,并绑定全局信号
const originalPromise = myPromise.resolve(item);
const cancelablePromise = new myPromise((res, rej) => {
originalPromise.then(
(value) => {
if (isSettled || signal.aborted) return;
isSettled = true;
raceController.abort(); // 3. 第一个任务完成,触发全局取消
res(value);
},
(error) => {
if (isSettled || signal.aborted) return;
isSettled = true;
raceController.abort(); // 错误也触发取消
rej(error);
}
);
}, { signal }); // 关键:传入全局信号
// 处理取消错误
cancelablePromise.catch((err) => {
if (err.message !== "Promise aborted" && !isSettled) {
isSettled = true;
raceController.abort();
reject(err);
}
});
});
});
}
}
// 测试优化后的 race 方法
myPromise.race([
// 慢任务:10秒后 resolve(会被取消)
new myPromise((resolve) => {
const timerId = setTimeout(() => {
console.log("慢任务:本应执行"); // 不会执行
resolve("慢任务结果");
}, 10000);
// 监听取消信号,清理定时器
this.#abortSignal?.addEventListener("abort", () => {
clearTimeout(timerId);
console.log("慢任务:已取消");
});
}),
// 快任务:2秒后 resolve
new myPromise((resolve) => {
setTimeout(() => {
console.log("快任务:执行完成");
resolve("快任务结果");
}, 2000);
})
]).then(res => console.log("race 结果:", res));
5.3 关键逻辑拆解
- 全局控制器创建:
race方法内创建raceController,所有参与竞争的任务共享其signal; - 任务绑定信号:每个任务通过
new myPromise(..., { signal })绑定全局信号,具备取消能力; - 第一个任务触发取消:当第一个任务完成(成功/失败),调用
raceController.abort(),全局信号变为aborted; - 慢任务自我清理:慢任务内部监听
signal.abort事件,收到信号后执行clearTimeout,终止定时器,不再执行resolve。
6. 兼容性与降级方案
AbortController 并非所有环境都支持,需针对低版本环境提供降级方案。
6.1 兼容性支持表
| 环境 | 支持版本 | 备注 |
|---|---|---|
| Chrome | 60+(2017年8月发布) | 完全支持 |
| Firefox | 57+(2017年11月发布) | 完全支持 |
| Safari | 12.1+(2019年3月发布) | 完全支持 |
| Edge(Chromium) | 79+(2020年1月发布) | 完全支持 |
| Edge(旧版) | 不支持 | 无原生支持,需降级 |
| IE 11 | 不支持 | 无原生支持,需降级 |
| Node.js | 15.0.0+(2020年10月) | 14.x 需开启 --experimental-abortcontroller 实验性标志 |
6.2 降级方案(自定义 CancelFlag)
对于不支持 AbortController 的环境,可通过“自定义 CancelFlag”模拟“协作式取消”,核心逻辑与 AbortController 一致,但不支持多任务批量监听。
降级示例(可取消定时器)
/**
* 降级版:可取消定时器(不依赖 AbortController)
* @param {number} delay - 延迟时间
* @param {any} value - resolve 值
* @returns {Object} { promise: Promise, cancel: Function }
*/
function createCancelableTimerPolyfill(delay, value) {
let isCanceled = false; // 自定义 CancelFlag
let timerId = null;
const timerPromise = new Promise((resolve) => {
timerId = setTimeout(() => {
if (isCanceled) {
console.log("定时器:已取消");
return;
}
resolve(value);
}, delay);
});
return {
promise: timerPromise,
cancel: () => {
isCanceled = true; // 标记为取消
if (timerId) clearTimeout(timerId); // 清理资源
console.log("定时器:已清理");
}
};
}
// 使用降级版
const { promise, cancel } = createCancelableTimerPolyfill(5000, "任务完成");
promise.then(res => console.log(res));
setTimeout(() => cancel(), 3000);
降级方案局限性
- 不支持多任务批量取消:需为每个任务维护独立的
isCanceled变量; - 无原生事件监听:需手动在每个任务步骤中判断
isCanceled; - 不支持
fetch/addEventListener原生绑定:需手动封装这些 API。
7. 总结
AbortController 是 JavaScript 异步编程的“标准化刹车系统”,核心价值在于:
- 协作式取消:通过信号通知任务“主动清理资源”,避免异步任务失控;
- 场景通用性:覆盖网络请求、定时器、事件监听、Promise 链等所有异步场景;
- 标准化接口:统一异步取消的实现方式,避免自定义方案的碎片化。
掌握 AbortController 能有效解决“资源浪费”“逻辑混乱”“内存泄漏”等异步编程痛点,是前端进阶的必备技能。
更多推荐


所有评论(0)