在现代前端开发中,我们经常需要精确控制异步任务的执行顺序、生命周期和中断机制。本文将围绕“红绿灯”问题(异步变同步)、fetch 请求的中止,以及 AbortController 和 Promise 的巧妙应用,进行系统性深度解析。
一、Fetch 能不能中止?—— AbortController 与信号机制
1. 问题背景:内存泄漏风险
在 SPA(单页应用)中,一个常见场景是:
- 用户在组件 A 发起一个
fetch请求。 - 请求耗时较长,尚未返回。
- 用户切换路由,组件 A 被卸载。
- 此时,
fetch请求仍在后台进行。 - 当请求最终返回时,组件已不存在,若尝试更新状态(如
setState),就会导致内存泄漏或报错。
2. 解决方案:AbortController
fetch API 原生支持通过 signal 选项进行中止。AbortController 就是为此设计的。
// 1. 创建 AbortController
const controller = new AbortController();
const { signal } = controller; // 获取 signal 对象
// 2. 发起 fetch 请求,并传入 signal
fetch('/api/data', {
method: 'GET',
signal: signal // 关键:将 signal 绑定到请求
})
.then(response => response.json())
.then(data => {
// 组件可能已卸载,需检查
if (componentMounted) {
setData(data);
}
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求已被中止');
} else {
console.error('请求失败:', error);
}
});
// 3. 在组件卸载时中止请求(React 示例)
useEffect(() => {
return () => {
controller.abort(); // 中止请求,触发 AbortError
};
}, []);
3. AbortController 的核心机制
signal:一个AbortSignal对象,作为“信号旗”。传递给fetch、EventSource等异步操作。abort():调用此方法,会触发signal上的abort事件,所有监听该信号的操作都会收到中止通知。- 跨任务通用:不仅限于
fetch,还可用于setTimeout、WebSocket、自定义异步任务。
// 用于中止 setTimeout
const controller = new AbortController();
const timeoutId = setTimeout(() => {
console.log('Timeout!');
}, 5000);
// 在某个条件下中止
controller.signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
console.log('Timeout 已中止');
});
// 触发中止
controller.abort();
✅ 核心思想:信号(Signal)是一种“发布-订阅”模式,用于在异步任务间传递中断指令。
二、红绿灯问题:异步变同步的“顺序控制”
1. 问题描述
模拟红绿灯:
- 红灯亮 3 秒
- 黄灯亮 1 秒
- 绿灯亮 2 秒
- 循环往复
要求:按顺序执行,不能同时亮。
2. 关键技术:sleep 函数的实现
JavaScript 没有内置的 sleep 函数,但我们可以用 Promise 和 setTimeout 轻松实现:
// 箭头函数一行搞定
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 使用
async function trafficLight() {
while (true) {
console.log('🔴 红灯亮');
await sleep(3000); // 等待 3 秒
console.log('🟡 黄灯亮');
await sleep(1000); // 等待 1 秒
console.log('🟢 绿灯亮');
await sleep(2000); // 等待 2 秒
}
}
trafficLight();
3. 深入解析:await 如何实现“暂停”
sleep(3000)返回一个 Promise,该 Promise 在 3 秒后被resolve。await关键字会暂停async函数的执行,直到 Promise 状态变为fulfilled。- 这期间,JavaScript 引擎可以处理其他任务(如事件、渲染),不会阻塞主线程。
- 3 秒后,Promise
resolve,await表达式完成,函数继续执行下一行。
✅ 本质:
await将“等待”从阻塞式(Blocking)变为非阻塞式(Non-blocking)的异步等待。
4. 更复杂的红绿灯:支持中断
结合 AbortController,实现可中断的红绿灯:
async function trafficLightWithAbort(signal) {
const colors = [
{ color: '🔴', duration: 3000 },
{ color: '🟡', duration: 1000 },
{ color: '🟢', duration: 2000 }
];
let index = 0;
while (!signal.aborted) { // 检查是否被中止
const { color, duration } = colors[index % colors.length];
console.log(`${color} 亮`);
// 使用 Promise.race 实现可中断的 sleep
try {
await Promise.race([
sleep(duration),
new Promise((_, reject) => {
signal.addEventListener('abort', () => {
reject(new Error('Traffic light aborted'));
});
})
]);
} catch (error) {
if (error.message === 'Traffic light aborted') {
console.log('红绿灯已中止');
return;
}
}
index++;
}
}
// 使用
const controller = new AbortController();
trafficLightWithAbort(controller.signal);
// 在某个时刻中止
setTimeout(() => {
controller.abort();
}, 10000);
三、Promise 考题精析
1. 经典题目:Promise.resolve 与微任务队列
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1, 4, 3, 2
解析:
1和4同步执行。setTimeout的回调进入宏任务队列。Promise.then的回调进入微任务队列。- 当前宏任务(脚本)执行完后,先清空微任务队列(输出
3),再执行下一个宏任务(输出2)。
2. 高级题目:async/await 执行顺序
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end'); // 这行在 await 后,会进入微任务
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve();
}).then(() => console.log('promise2'));
console.log('script end');
// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
关键点:await 后的代码会被包装成微任务。
四、总结:异步控制的三大支柱
| 技术 | 作用 | 核心场景 |
|---|---|---|
Promise | 封装异步操作,解决回调地狱 | 基础异步处理 |
async/await | 以同步语法书写异步代码,await 实现“暂停” | 顺序控制、简化逻辑 |
AbortController / signal | 发送中断信号,主动取消异步任务 | 防止内存泄漏、用户取消操作 |
最佳实践:
- 用
sleep = ms => new Promise(r => setTimeout(r, ms))实现等待。 - 用
await控制异步执行顺序,实现“红绿灯”等场景。 - 在组件生命周期中(如
useEffectcleanup),使用AbortController.abort()中止未完成的fetch请求,防止内存泄漏。 - 理解事件循环:宏任务(
setTimeout) vs 微任务(Promise.then),这是掌握异步执行顺序的基础。
掌握这些技术,你就能像交通警察一样,精准地指挥前端应用中错综复杂的异步任务流。