请求竞态与取消
请求竞态与取消 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课解决异步请求的经典事故:快速切换时,先发出但更慢的旧请求最后到达,把新页面覆盖掉。读完你应该能解释竞态的成因,并会用清理函数与 AbortController 两种手段保证「界面只认最后一次请求」。
问题场景
课程详情页顶部有两个课程标签,点击后请求接口拿详情。为了模拟真实网络,「课程 1」配了 3 秒延迟,「课程 2」只配 300 毫秒。先点课程 1 再马上点课程 2:
// 有竞态的版本:谁后到谁说了算
function CoursePage({ courseId }) {
const [course, setCourse] = useState(null);
useEffect(() => {
fetchCourse(courseId).then(setCourse); // 慢的旧响应也会 setCourse
}, [courseId]);
// ...
}
结果:界面先显示课程 2,3 秒后又被课程 1 的响应覆盖——页面停在了一个你没点过的课程上。
核心解释
竞态(race condition)指多个异步操作的结果到达顺序和发起顺序不一致,后发先至。请求有快有慢,React 不会替你判断哪个响应是「最新的」,两次 fetchCourse 都会执行 then(setCourse),谁晚到谁覆盖谁。
修复思路是给每轮请求定一条归属规则:只有最后一次发起的请求才有资格写状态。useEffect 的清理函数正好提供时机——下一次 Effect 运行前、以及组件卸载前,都会先执行上一轮的清理:
useEffect(() => {
let ignore = false; // 本轮请求的「通行证」
fetchCourse(courseId).then((data) => {
if (!ignore) setCourse(data); // 过期的请求拿不到通行证
});
return () => {
ignore = true; // courseId 变了或组件卸载:本轮作废
};
}, [courseId]);
ignore 声明在 Effect 内部,每轮各发一张。courseId 从 1 变成 2 时,先跑第一轮的清理(作废 1 的通行证),再跑第二轮(发新证)。于是课程 1 的响应再慢也写不进来。
如果想连请求本身都省掉,用 AbortController:把 signal 传给 fetch,清理时 abort(),浏览器会真正中断连接,catch 里会收到 AbortError——注意别把它当故障弹给用户。
示例拆解
本课实验区是可运行的对照版本,用开关切换两种行为:
useEffect(() => {
let ignore = false;
setLoading(true);
fetchCourse(courseId).then((data) => {
if (!ignore || buggy) { // buggy=true 时跳过归属检查,复现事故
setCourse(data);
setLoading(false);
}
});
return () => { ignore = true; };
}, [courseId, buggy]);
fetchCourse在实验里用setTimeout模拟延迟,课程 1 慢、课程 2 快,和真实网络行为一致。- 「已修复」模式:点课程 1 再立刻点课程 2,界面最终停在课程 2,课程 1 的响应被
ignore拦下。 - 「有 bug」模式:同样操作,3 秒后界面被课程 1 覆盖,复现事故。
- 真实项目里把
fetchCourse换成fetch(url, { signal })并在清理里controller.abort(),归属策略不变。
动手任务
打开本课实验区:
- 勾选「有 bug 模式」,点课程 1 后 0.5 秒内点课程 2,等 3 秒观察覆盖现象,用自己的话描述发生了什么。
- 取消勾选重复同样操作,确认界面最终停在课程 2,且加载文案里显示的 id 始终是最后点击的。
- 进阶:把
fetchCourse改成接收signal,在清理函数里abort(),确认取消后不再出现「丢弃响应」的时机——这一步在本地项目里做更接近真实。
常见错误
- 把清理函数写成异步的:清理必须是同步函数,
abort()要直接写在函数体里,不能return一个 Promise。 ignore声明在组件体里:每次渲染都重新执行组件函数,组件体里的变量跟着重置,通行证失效;必须在 Effect 内部声明。- abort 后照常提示错误:
catch里要先判断error.name === 'AbortError',主动取消不是故障。 - 以为取消能替代一切:
ignore归属策略不依赖浏览器支持,任何异步任务都能用;AbortController 只是额外省流量,两者互补而非二选一。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能说清竞态成因:响应到达顺序不等于发起顺序,React 不会自动挑选最新响应
- 知道清理函数的两个触发时机:下一次 Effect 之前、组件卸载之前
- 能说出两种策略的分工:
ignore管归属,AbortController管取消
参考资料
- React 官方文档:Effect 的同步逻辑 —— 「Fetching data」一节正是本课场景的官方解法
- React 官方文档:useEffect —— 清理函数的触发时机与语义
- MDN:AbortController —— 取消 fetch 的标准接口
读完这一课了?
标记完成后,工作台会安排接下来的学习。