副作用清理与生命周期
副作用清理与生命周期 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
Effect 建立连接,清理函数负责断开。读完你应该能实现订阅的注册与清理,说清清理函数的三个触发时机,并用「监听器计数」验证切换页面、卸载组件后不残留重复监听。
问题场景
消息面板挂载时订阅一个消息源,每来一条消息就追加显示。第一版忘了写清理:
function ChatPanel() {
const [msgs, setMsgs] = useState([]);
useEffect(() => {
subscribe(m => setMsgs(prev => [...prev.slice(-4), m]));
// 没有返回清理函数
}, []);
return <div>{msgs.join("、")}</div>;
}
打开面板 → 关掉 → 再打开,消息开始一条顶两条;再开关一轮,一条顶四条。面板明明只挂载了一个,监听回调却在订阅名单里越积越多。
核心解释
订阅这类外部连接,Effect 的标准形态是建立 + 清理成对出现:
useEffect(() => {
const unsubscribe = subscribe(m => setMsgs(prev => [...prev.slice(-4), m]));
return unsubscribe; // 返回清理函数
}, []);
清理函数会在三个时机被调用,每个时机都有明确含义:
- 组件卸载时:面板销毁,断开订阅,不留孤儿监听;
- 依赖变化、Effect 重跑前:先清理旧连接再建新连接,保证任何时刻只有一份有效订阅(R24 的定时器重建就是这个节奏);
- 严格模式的开发环境:挂载后会立刻「挂载 → 清理 → 再挂载」走一遍,专门帮你暴露漏清理的 bug——这也是为什么开发时经常看到 Effect 跑了两次。
判断要不要清理的口诀:Effect 让世界多了一个东西(监听器、定时器、连接、占用的资源),清理函数就负责把它收走。收走之后,组件无论怎么挂载、卸载、重跑,外部世界都不会留下残渣。上一课订阅名单里积攒的重复回调,正是「建立」没有配「断开」的直接后果。
还有一个细节值得注意:订阅回调里用的是 setMsgs(prev => [...]) 函数式更新,不依赖渲染快照——这让 Effect 可以安全地只挂载一次,配合空依赖数组成立(R24 的出路在这里落地)。
示例拆解
实验区模拟了一个消息源,并把「订阅名单」直接显示出来:
- 顶部显示当前监听器数量,每秒广播一条新消息;
- 「打开聊天面板」挂载
ChatPanel,Effect 里 subscribe;开关按钮反复切换组件的挂载与卸载; - 有个开关能在「正确版」和「错误版」之间切换:错误版 subscribe 之后不返回清理函数;正确版返回 unsubscribe;
- 用错误版开 3 次面板:监听器数量变成 3,面板里每条消息重复显示 3 份——因为 3 个回调都在往同一个列表追加;
- 切到正确版重新开关:监听器数量回到 1,消息永远只有一份;
- 验证方式:两种模式各开关 3 次,对比监听器计数和消息重复份数。
动手任务
打开本课实验区,完成三个小改动:
- 在 ChatPanel 的 Effect 里加一条日志(或界面提示),分别在「建立订阅」和「清理订阅」时触发,亲眼确认清理时机。
- 把面板里的消息列表改成最多保留 6 条(现在是 4 条),并显示「已订阅 / 未订阅」状态。
- 给 App 顶层的广播定时器也加上清理(它现在已经在做了),说出如果漏掉、在实验区反复挂载时会观察到什么。
常见错误
- 清理函数写错返回方式:
useEffect(() => { subscribe(fn); return unsubscribe; })对;写成return unsubscribe()是当场执行订阅立刻断开,回调永远不触发。 - 清理的是另一个连接:subscribe 返回的引用被覆盖或丢失,清理时断不开原订阅;用变量接住返回值再原样返回。
- 以为依赖数组为空就不用清理:空数组只说明「连接参数不变」,组件卸载时照样需要断开;清理和依赖频率无关。
- 在清理函数里再做一次 setState:卸载后更新已卸载组件没有意义还容易出警告;清理只负责对外断开,不负责对内改状态。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能背出清理函数的三个触发时机:卸载、依赖变化重跑前、严格模式模拟
- 每写一个建立连接的 Effect,都顺手返回清理函数
- 会用外部可观察的信号(监听器计数、消息重复份数)验证清理是否干净
参考资料
- React 官方文档:使用 Effect 进行同步 —— 建立/清理的完整生命周期讲解
- React 官方文档:useEffect 返回值 —— 清理函数的 API 约定
读完这一课了?
标记完成后,工作台会安排接下来的学习。