依赖陷阱与旧闭包
依赖陷阱与旧闭包 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
定时器跑着跑着用的是三秒前的旧值、订阅回调里读到的永远是 0——这些都是旧闭包。这一课你会亲手复现它,并掌握三种修法的适用场景:函数式更新、正确的依赖数组、ref 持有最新值。
问题场景
一个每秒自增的计数器:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 用的永远是首次渲染的 count = 0
}, 1000);
return () => clearInterval(id);
}, []); // 依赖数组撒了谎:回调明明用了 count
return <p>{count}</p>;
}
期望每秒加一,实际停在 1。useEffect 的回调在首次渲染时创建,捕获的 count 是那次渲染快照里的 0(快照模型见 R08)。依赖数组写 [] 等于向 React 承诺「回调不依赖任何会变的值」——这个承诺被打破了,React 也无从帮你。
核心解释
修法有三条,按优先级选:
一、函数式更新:根本不需要旧值。 新值只由旧值算出时,别引用快照:
setCount((c) => c + 1); // c 由更新队列提供,永远是最新的
二、正确的依赖:值变了就重新同步。 回调确实需要 count 时如实声明,Effect 会在 count 变化后清掉旧定时器、建新的。功能正确,代价是 interval 每秒被销毁重建——对长期订阅不划算。
三、ref 持有最新值:既要长期订阅、又要读最新值。 用一个小 Effect 把快照同步进 ref,回调读 ref:
const countRef = useRef(0);
useEffect(() => { countRef.current = count; }, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log('最新值:', countRef.current); // 不触发渲染,只是读
}, 1000);
return () => clearInterval(id);
}, []);
顺带一提:React 正在实验的 useEffectEvent 就是为「读最新值但不重跑 Effect」设计的,可以关注,但生产代码目前仍以上面三种为准。
示例拆解
实验区提供三种模式的定时器对照,以及「延迟 3 秒 +1」按钮——演示同类问题在事件回调里的变体:
function delayedAddBroken() {
setTimeout(() => setCount(count + 1), 3000); // 捕获的是点击那一刻的快照
}
function delayedAddFixed() {
setTimeout(() => setCount((c) => c + 1), 3000); // 执行时才取最新值
}
- 3 秒内连点两次坏版按钮,两次加的都是同一个旧快照,结果只加 1。
- 修复版两次各自基于到期那一刻的最新值,结果加 2。
- 事件回调里的闭包问题不涉及依赖数组——快照捕获发生在函数创建时,与 Effect 无关,但病根相同。
动手任务
打开本课实验区:
- 依次切换「坏版 / 函数式 / 正确依赖」三种模式各观察 5 秒,说出坏版停在 1、其余正常递增的原因。
- 分别连点两次「延迟 +1(坏)」和「延迟 +1(修复)」,对比 3 秒后的结果差。
- 切到「正确依赖」模式看屏幕日志,观察 interval 随
count变化被重建的节奏。
常见错误
- 依赖数组撒谎:写
[]却用了 state,编译不报错、运行必出鬼;要么函数式更新,要么如实声明依赖。 - 依赖写对象或函数:
[{ id }]或[onChange]每次渲染都是新引用,Effect 每次渲染都重跑;传原始值,或用 useMemo/useCallback 稳定引用。 - 补全依赖后过度重订阅:依赖对了,但 WebSocket 每 100ms 重建一次——该改用函数式更新或 ref 方案。
- 以为闭包问题只在 useEffect 里:事件处理里的 setTimeout、Promise 回调一样捕获快照,修法同样是函数式更新。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能解释旧闭包成因:回调捕获的是创建那一刻的渲染快照
- 三种修法能对号入座:不需要值用函数式;值变了要重同步用正确依赖;长期订阅又要最新值用 ref
- 审查 Effect 时先看依赖数组有没有「撒谎」
参考资料
- React 官方文档:Effect 的同步逻辑 —— 依赖数组与重新同步的官方语义
- React 官方文档:队列化一系列状态更新 —— 函数式更新的原理
- React 官方文档:useEffect —— API 参考
读完这一课了?
标记完成后,工作台会安排接下来的学习。