副作用与引用综合实验
副作用与引用综合实验 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课没有新概念,是一次综合验收:把请求竞态(R26)、引用管理(R27)、旧闭包(R30)装进同一个页面——可暂停番茄钟加可切换的课程详情,并把「卸载清理」当成一条正式验收线。
问题场景
页面结构:左侧课程标签(3 个,模拟接口延迟不同),右侧番茄钟(可开始 / 暂停 / 重置)。前几课的事故在这里都有条件复发:快速切换课程时旧响应覆盖新页面;番茄钟回调读到旧状态;整页卸载后定时器和请求还在跑。第一版代码的清理台账长这样——空的:
useEffect(() => {
fetchCourse(courseId).then(setCourse); // 竞态:没有归属检查
}, [courseId]);
useEffect(() => {
if (!running) return;
const id = setInterval(tick, 1000); // tick 读旧快照,且没有 clearInterval
}, [running]);
核心解释
综合实验的要点不是写更多代码,而是给每个副作用建立清理台账,动笔前先列出来:
| 副作用 | 启动 | 清理 |
|---|---|---|
| 课程详情请求 | fetchCourse(courseId) | ignore = true(或 abort) |
| 计时 interval | setInterval(tick, 1000) | clearInterval(id) |
| 全局监听(如有) | addEventListener | removeEventListener |
验证方法也要提前设计,本课分三条线:
- 竞态线:点最慢的课程,0.5 秒内点最快的,详情必须停在最后点击的那个。
- 旧闭包线:番茄钟运行中「暂停」再「开始」,秒数必须连续,不能跳回或加倍走。
- 卸载线:实验区提供「卸载整个页面」开关;卸载前让请求和计时都在飞行中,卸载后屏幕日志应出现清理记录,且之后再无状态写入。
顺带纠正一个旧观念:React 18 之后「卸载后 setState」的警告已经移除,但这不代表泄漏无害——定时器和回调仍占内存、仍会执行,白耗资源还可能操作已失效的 DOM。
示例拆解
番茄钟的核心:running、seconds 用 state(界面显示),interval id 交给 ref,回调里用函数式更新:
function Pomodoro() {
const [seconds, setSeconds] = useState(150);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
const id = setInterval(() => {
setSeconds((s) => (s <= 1 ? 0 : s - 1)); // 不依赖外部快照
}, 1000);
return () => clearInterval(id); // 台账第 2 行:暂停、卸载都走这里
}, [running]);
return (
<div>
<p>{Math.floor(seconds / 60)} 分 {seconds % 60} 秒</p>
<button onClick={() => setRunning((r) => !r)}>{running ? '暂停' : '开始'}</button>
</div>
);
}
课程区沿用 R26 的 ignore 归属策略,并在过期响应到达时往屏幕日志写一条记录:
fetchCourse(courseId).then((data) => {
if (ignore) { log(`丢弃了课程 ${data.id} 的过期响应`); return; }
setCourse(data);
setLoading(false);
});
整页组件再提供一个顶层开关控制挂载 / 卸载;卸载日志由各 Effect 的清理函数写入,这就是「清理真的执行了」的证据。
动手任务
打开本课实验区,按三条验收线逐项操作:
- 竞态:点「课程 A(慢)」立刻点「课程 C(快)」,确认详情停在 C,日志出现「丢弃了课程 A 的过期响应」。
- 旧闭包:番茄钟走 5 秒,暂停再开始,确认秒数连续不跳变。
- 卸载:让慢请求飞行中、计时运行中,点「卸载整个页面」,确认日志出现「清除了计时器」和「丢弃过期响应」,重新挂载后一切从干净状态开始。
常见错误
- 清理写一半:请求作了废、interval 忘清,泄漏照样发生;对照台账逐行检查。
- 清理代码和副作用分家:谁发起谁清理,清理必须和启动写在同一个 Effect 里,跨组件代管必漏。
- 重挂载后状态没复位:要复位的状态放在会被卸载的组件内部,别提到外面全局存。
- 只用 console 验证:沙箱里更要看屏幕日志,确认清理发生的时间点符合预期。
检查与复盘
完成下方检查题确认理解。自查清单:
- 三条验收线(竞态、旧闭包、卸载清理)全部按清单走过一遍
- 能为页面里每个 useEffect 写出「启动 / 清理」对照表
- 知道卸载后 setState 不再报警告,但清理仍是硬要求
参考资料
- React 官方文档:Effect 的同步逻辑 —— 清理函数语义的最终参考
- React 官方文档:useEffect —— API 参考
- React 官方文档:useRef —— 计时器 id 与最新值持有
读完这一课了?
标记完成后,工作台会安排接下来的学习。