第四节:React Hooks进阶篇-useEffect依赖项为空数组[]与不写的区别

发布于:2025-04-13 ⋅ 阅读:(26) ⋅ 点赞:(0)

陷阱题:闭包问题、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);
}, []);

原理:通过函数参数获取最新状态值,无需依赖项。


四、最佳实践与性能优化
  1. 依赖项规则:确保所有引用的外部变量(包括propsstate)都出现在依赖数组中。
  2. 避免对象/数组依赖:直接传递对象属性或使用useMemo优化引用类型。
  3. 并发模式兼容:React 18中,useEffect可能被中断渲染,优先使用useLayoutEffect处理DOM同步操作。

五、总结对比表
场景 空数组[] 无依赖项
典型用途 初始化请求、一次性订阅 响应式DOM操作、全局事件监听
闭包风险 高(依赖旧值) 低(每次更新生成新闭包)
性能影响 低(仅执行一次) 高(频繁触发)

通过合理选择依赖项策略,可显著提升组件性能和逻辑健壮性。