前言
今天主要围绕前端开发中一个非常核心但又容易被忽视的问题展开:
请求竞态 & 并发控制
这类问题在实际项目(尤其是搜索、列表加载、数据请求)中非常常见,同时也是面试高频考点。
本文会从基础到进阶,系统梳理:
- 什么是请求竞态
- 如何解决竞态问题
- AbortController 的底层原理
- React 中如何优雅处理请求
- 什么是并发请求
- 如何实现并发控制(核心算法)
一、什么是请求竞态(Race Condition)
典型场景:搜索框
用户输入:
a → ab → abc
前端发出请求:
req1: a
req2: ab
req3: abc
但返回顺序可能是:
req3 → req1 → req2
最终 UI 显示的是旧数据(比如 ab 或 a)。
本质
请求的返回顺序 ≠ 请求的发送顺序
二、解决请求竞态的三种方案
方案1:请求打标(版本控制)
思路
每个请求分配唯一 ID(或时间戳),只处理"最新请求"的结果。
示例代码
js
let currentRequestId = 0;
function search(keyword) {
const requestId = ++currentRequestId;
fetch(`/api?q=${keyword}`)
.then(res => res.json())
.then(data => {
if (requestId !== currentRequestId) return;
render(data);
});
}
优点
- 简单稳定
- 不依赖浏览器能力
缺点
- 请求仍然发送(浪费资源)
方案2:取消请求(AbortController)
思路
每次新请求前取消旧请求。
js
let controller;
function search(keyword) {
if (controller) controller.abort();
controller = new AbortController();
fetch(`/api?q=${keyword}`, {
signal: controller.signal
});
}
注意需要过滤取消错误:
js
.catch(err => {
if (err.name === 'AbortError') return;
});
优点
- 真正取消请求
- 更节省资源
方案3:防抖(Debounce)
思路
用户停止输入一段时间后才发请求,从源头减少请求数量。
三、AbortController 底层原理
核心机制
fetch监听signalabort()触发事件- 浏览器网络层中断请求
本质
JS 发出信号 → 浏览器执行中断
不同阶段的行为
| 阶段 | 行为 |
|---|---|
| 请求未发送 | 直接取消 |
| 请求中 | 中断连接 |
| 响应中 | 停止读取数据 |
四、React 中的正确写法
错误示例
jsx
useEffect(() => {
fetch(`/api?q=${keyword}`)
.then(res => res.json())
.then(data => setData(data));
}, [keyword]);
问题:
- 请求竞态
- 组件卸载后仍 setState(内存泄漏)
正确写法
jsx
useEffect(() => {
const controller = new AbortController();
fetch(`/api?q=${keyword}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => setData(data))
.catch(err => {
if (err.name === 'AbortError') return;
console.error(err);
});
return () => {
controller.abort();
};
}, [keyword]);
核心点
- 每次 effect 执行 → 创建 controller
- cleanup 函数 → 取消上一次请求
五、什么是并发请求
定义
多个请求同时需要执行,而不是互相替代。
示例
js
fetch('/user');
fetch('/posts');
fetch('/notifications');
和竞态的区别
| 类型 | 特点 | 是否需要取消 |
|---|---|---|
| 竞态 | 新请求替代旧请求 | 是 |
| 并发 | 多请求同时需要 | 否 |
六、并发问题的核心
并发场景要解决的不是"取消",而是:
- 结果管理
- 错误隔离
- 并发数量控制
七、实现并发控制(重点)
目标
限制同时最多 N 个请求。
核心思想
任务池 + 补位机制
完整实现(推荐掌握)
js
function limitRequests(tasks, limit) {
return new Promise((resolve) => {
let i = 0;
let finished = 0;
const results = [];
function run() {
if (i >= tasks.length) return;
const currentIndex = i;
const task = tasks[i++];
task()
.then((res) => {
results[currentIndex] = res;
})
.catch((err) => {
results[currentIndex] = err;
})
.finally(() => {
finished++;
if (finished === tasks.length) {
resolve(results);
} else {
run(); // 补位
}
});
}
for (let j = 0; j < limit; j++) {
run();
}
});
}
执行流程
- 先执行 limit 个任务
- 任意任务完成 → 补一个新任务
- 循环直到完成
八、整体知识体系总结
三类问题及解法:
| 问题 | 解决方案 |
|---|---|
| 请求竞态 | 打标 / AbortController |
| 并发请求 | Promise.all / allSettled |
| 并发过多 | 并发控制(limit) |
九、面试总结话术
请求竞态
通过请求打标或 AbortController 控制请求生命周期,避免旧请求覆盖新数据。
AbortController
基于事件机制,通过 signal 通知浏览器网络层中断请求。
并发控制
通过维护执行池,控制最大并发数,并在任务完成后动态补位。
后续学习方向
下一步可以继续深入:
- 并发控制 + 重试机制
- 请求超时设计
- 请求缓存(Cache)
- React Hook 封装(useRequest)
总结
今天你已经掌握了一条非常重要的前端能力链路:
请求竞态 → 请求取消 → React 实战 → 并发理解 → 并发控制
这套知识在实际开发和面试中都非常有价值。
下一步建议:请求库封装(工程级 useRequest 实现),将本文所有知识整合成一个真实项目级方案。