陷阱题:闭包问题、Stale Closure举例
一、依赖项为空数组[]
与不写的核心区别
行为 | 空数组[] |
不写依赖项 |
---|---|---|
执行时机 | 仅在组件挂载时执行一次(类似componentDidMount ) |
组件每次渲染后都执行(类似componentDidUpdate ) |
更新触发条件 | 永不触发(除非组件卸载后重新挂载) | 任何状态或属性变化都会触发 |
清理函数执行时机 | 仅在组件卸载时执行一次 | 每次重新渲染前都会执行清理函数 |
二、闭包陷阱(Stale Closure)示例
案例1:定时器中的旧值引用
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// 闭包陷阱:始终引用初始值0
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖项为空数组
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
• 问题:定时器回调函数中的count
始终是初始值0
,因为闭包捕获了初始渲染时的状态。
• 原因:空数组依赖项导致useEffect
仅执行一次,内部闭包未更新。
案例2:依赖项缺失导致无限循环
useEffect(() => {
setCount(count + 1); // 未设置依赖项,每次渲染触发更新
});
• 结果:组件陷入无限渲染循环。
三、闭包问题的解决方案
方案1:正确设置依赖项
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 每次count变化时获取最新值
}, 1000);
return () => clearInterval(timer);
}, [count]); // 依赖项包含count
• 原理:依赖项变化时,重新生成闭包函数。
方案2:使用useRef
绕过闭包
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 手动同步最新值到ref
});
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current); // 通过ref获取最新值
}, 1000);
return () => clearInterval(timer);
}, []);
• 优势:避免依赖项导致定时器频繁重置。
方案3:函数式更新状态
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // 函数式更新,避免依赖旧值
}, 1000);
return () => clearInterval(timer);
}, []);
• 原理:通过函数参数获取最新状态值,无需依赖项。
四、最佳实践与性能优化
- 依赖项规则:确保所有引用的外部变量(包括
props
和state
)都出现在依赖数组中。 - 避免对象/数组依赖:直接传递对象属性或使用
useMemo
优化引用类型。 - 并发模式兼容:React 18中,
useEffect
可能被中断渲染,优先使用useLayoutEffect
处理DOM同步操作。
五、总结对比表
场景 | 空数组[] |
无依赖项 |
---|---|---|
典型用途 | 初始化请求、一次性订阅 | 响应式DOM操作、全局事件监听 |
闭包风险 | 高(依赖旧值) | 低(每次更新生成新闭包) |
性能影响 | 低(仅执行一次) | 高(频繁触发) |
通过合理选择依赖项策略,可显著提升组件性能和逻辑健壮性。