快速、频繁请求造成的数据混乱问题。
在 JavaScript 中处理请求竞态问题(即防止旧请求覆盖新请求结果)有以下几种优雅方案,按推荐程度排序:
方案 1:使用 AbortController (现代浏览器标准方案)
let controller = null;
async function fetchData(query) {
// 取消上一个未完成的请求
if (controller) controller.abort();
controller = new AbortController();
try {
const response = await fetch(`/api?q=${query}`, {
signal: controller.signal, // 绑定取消信号
});
const data = await response.json();
// 处理数据(此时确保是最新请求)
return data;
} catch (err) {
// 忽略被主动取消的请求错误
if (err.name !== "AbortError") throw err;
}
}
// 使用示例
searchInput.addEventListener("input", (e) => {
fetchData(e.target.value);
});
方案 2:请求 ID 标记法 (兼容性好)
let currentRequestId = 0;
async function fetchData(query) {
const requestId = ++currentRequestId;
const response = await fetch(`/api?q=${query}`);
const data = await response.json();
// 只处理最新请求的结果
if (requestId === currentRequestId) {
// 渲染数据
}
}
// 使用同上
方案 3:Promise 状态锁 (纯 Promise 实现)
let latestPromise = null;
async function fetchData(query) {
const thisPromise = fetch(`/api?q=${query}`)
.then(r => r.json());
latestPromise = thisPromise;
const result = await thisPromise;
// 只处理最新的 Promise
if (thisPromise === latestPromise) {
// 处理数据
}
}
方案 4:RxJS (响应式编程方案)
import { fromEvent } from "rxjs";
import { switchMap } from "rxjs/operators";
fromEvent(searchInput, "input")
.pipe(
switchMap((e) => fetch(`/api?q=${e.target.value}`).then((r) => r.json()))
)
.subscribe((data) => {
// 自动取消前序请求,只处理最新
});
方案 5:React Hooks 封装 (React 专属)
function useRaceSafeFetch() {
const controllerRef = useRef();
return async (url) => {
// 取消上一个请求
if (controllerRef.current) {
controllerRef.current.abort();
}
const controller = new AbortController();
controllerRef.current = controller;
try {
const res = await fetch(url, { signal: controller.signal });
return res.json();
} catch (err) {
if (err.name !== "AbortError") throw err;
}
};
}
// 在组件中使用
function MyComponent() {
const safeFetch = useRaceSafeFetch();
const handleSearch = (query) => {
safeFetch(`/api?q=${query}`).then((data) => {
// 安全更新状态
});
};
}
最佳实践建议:
现代项目首选
AbortController
- 原生支持且符合标准框架集成:
- React:结合
useEffect
+AbortController
- Vue:在组件卸载时自动取消 (
onUnmounted
)
- React:结合
错误处理:务必检查
error.name === 'AbortError'
性能优化:添加防抖避免频繁取消请求
// 配合防抖使用 (lodash.debounce) const debouncedFetch = debounce(fetchData, 300);
防抖 + AbortController 完整示例
function createRaceSafeFetcher() {
let controller = null;
let lastCall = 0;
return async (query) => {
// 防抖:300ms 内的请求视为同一批次
const now = Date.now();
if (now - lastCall < 300) {
if (controller) controller.abort();
}
lastCall = now;
controller = new AbortController();
try {
const res = await fetch(`/api?q=${query}`, {
signal: controller.signal,
});
return res.json();
} catch (err) {
if (err.name !== "AbortError") throw err;
}
};
}
// 使用
const safeFetch = createRaceSafeFetcher();
searchInput.addEventListener("input", (e) => safeFetch(e.target.value));
选择方案的建议:
- 浏览器项目:优先使用
AbortController
- 兼容旧浏览器:请求ID标记法
- React项目:自定义Hook封装
- 复杂异步流:考虑RxJS
这些方案都能有效解决请求竞态问题,核心思路都是 "使旧请求失效" 或 "忽略旧响应"。实际项目中建议根据技术栈和浏览器支持情况选择最合适的方案。