【React】闭包陷阱
所谓 React 中的闭包陷阱(Closure Trap),其实是 函数组件里的事件回调或副作用中,引用了过期的 state 或 props,导致逻辑异常。
1. 为什么 React 里会有闭包陷阱?
- React 函数组件 本质上是一个 render 函数。
- 每次渲染都会生成新的 props / state,以及新的 函数作用域。
- 如果某个回调函数(比如事件处理函数、
setTimeout里的回调、useEffect里的函数)引用了旧的 state/props,就会形成一个 闭包,而这个闭包“锁死”了旧的值。
2. 典型例子
import { useEffect, useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return (
<div>
<p>{count}</p>
</div>
);
}
页面只会从 0 → 1,之后 不再递增
useEffect([]) 只在首次渲染执行一次,此时 count === 0
setInterval 回调里 闭包捕获的是“第一次 render 时的 count”
后续 render 虽然 count 变了,但 interval 里的 count 永远是 0
useEffect 只会在第一次 commit 时候执行回调,后续继续使用 count === 0 ,然后 0 + 1 这个旧值,所以我们要么手动添加 count 依赖项,要么 通过回调函数的 setState 方式修改,不断获取最新的 state。
👉 本质:副作用函数中使用了旧状态
3. 解决办法
✅ 方法一:使用函数式更新(推荐)
setCount(prev => prev + 1);
prev是 React 在 render 时传入的最新 state- 不依赖闭包
- 并发安全
✅ 方法二:依赖数组正确写法(useEffect)
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
👉 保证每次 count 变了,副作用都能捕获到新值。
问题:
- 每次 count 变化
- 都会清除 + 重建 interval
- 性能和语义都不好
✅ 方法三:使用 useRef 保存最新值
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
👉 ref 不会因为重新渲染而丢失,适合存放“跨渲染保持的最新值”。适合复杂场景(如 WebSocket / 事件监听),但简单计数器没必要
4. 总结
-
闭包陷阱原因:函数组件每次渲染都会创建新的作用域,回调函数可能捕获旧的 props/state。
-
常见场景:
setTimeout、setInterval、事件回调、异步请求回调、useEffect。 -
解决办法:
- 函数式更新
setState(prev => ...)(推荐!) - 正确声明依赖(
useEffect) - 使用
useRef存最新值
- 函数式更新
5. 从底层来看这个问题
第一次 render(count = 0)
const [count, setCount] = useState(0);
- count = 0
- React 创建 Fiber
- effect 被注册(但还没执行)
页面显示:
<p>0</p>
commit 阶段 → 执行 useEffect
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
⚠️ 注意:
count是 第一次 render 的快照- 此时:
count === 0
等价于:
setInterval(() => {
setCount(0 + 1);
}, 1000);
1 秒后,interval 第一次执行
setCount(1);
React 内部:
- enqueue update
- 触发 render
第二次 render(count = 1)
<p>1</p>
⚠️ 但注意:
- effect 不会重新执行
- interval 里的回调 还是老的闭包
第 2 秒、第 3 秒、第 N 秒……
每一次 interval 执行的都是:
setCount(0 + 1);
React 发现:
- state 已经是 1
- 再 set 1 → 无变化
- React 会 bail out(跳过更新)
📌 所以 count 卡死在 1
闭包捕获的是 render 时的快照,而不是 Fiber 上的最新 state
深入分析
const count = hook.memoizedState; // 当前 render 的值
- 每次 render 都是新的
count - effect 中的回调,只绑定了一次 render 的 count
useEffect(..., []);
等价于:
“我保证 effect 不依赖任何 render 中的变量”
但实际上你偷偷依赖了 count ❌
为什么 React 不帮你“自动更新 count”?
因为:
- render 可以被并发中断
- effect 回调可能被丢弃
- React 不能在 effect 中隐式追踪变量变化
📌 这就是 Hooks 要求你显式声明依赖的原因
更多推荐



所有评论(0)