React useEffect 不传第二个参数会怎么样

FreeGuideOnline 最新 2026-07-05

React useEffect 不传第二个参数会发生什么?

在 React 函数组件中,useEffect 是处理副作用的核心 Hook。它的调用方式直接影响组件的执行时机和性能。当你完全省略第二个参数时,会触发一种特殊的行为,这往往是导致无限循环或性能问题的常见原因。

1. 参数回顾:三种依赖项传递方式

在讨论不传第二个参数之前,先回顾 useEffect 的三种基本写法:

写法 依赖项参数 副作用执行时机
useEffect(() => { ... }) 不传第二个参数 每次渲染后都会执行
useEffect(() => { ... }, []) 传递空数组 仅在组件挂载时执行一次
useEffect(() => { ... }, [dep]) 传递依赖数组 仅在 dep 变化时执行(含挂载)

2. 不传第二个参数:每次渲染后都运行

当你不传递第二个参数(即完全不写依赖数组)时,useEffect 中的回调函数会在每一次组件渲染完成后被调用。

useEffect(() => {
  console.log('副作用执行,当前渲染次数:', renderCount);
});

这等同于在类组件中没有添加任何条件,直接在 componentDidMountcomponentDidUpdate 里都执行相同的逻辑。

  • 挂载时:组件第一次渲染到 DOM 后,回调执行一次。
  • 更新时:每次因为 state 或 props 变化导致重新渲染后,回调都会再次执行。

3. 为什么会导致无限循环?

这是初学者最常踩的坑。如果你在副作用中更新了 state,且该 state 的变化会触发组件的重新渲染,那么就会陷入死亡循环

function Counter() {
  const [count, setCount] = useState(0);

  // 🚨 危险:不传依赖项,每次渲染后都执行
  useEffect(() => {
    setCount(count + 1); // 改变了 state,触发重新渲染
  });

  return <div>{count}</div>;
}

执行流程:

  1. 组件首次渲染,count 为 0,副作用运行 → setCount(1)
  2. count 变为 1,触发第二次渲染。
  3. 第二次渲染后,副作用再次运行 → setCount(2)
  4. 这个过程无限重复,直到浏览器卡死或报错。

4. 与“传空数组”的关键区别

很多开发者误以为“不传参数”和“传空数组”效果类似,实则完全不同:

  • 空数组 []:告诉 React 这个副作用不依赖于任何 props 或 state,所以只会执行一次(挂载时),相当于 componentDidMount
  • 不传第二个参数:意味着只要组件渲染,副作用就必须执行。React 完全不跳过这个效果,每次都会重新运行它。

5. 什么时候会故意不传第二个参数?

通常情况下,不传依赖数组是一个危险的模式,应该极力避免。但在极少数场景下,你可能需要这种“每次渲染都执行”的行为:

  • 记录渲染次数或日志:用于调试或监控,尽管现在更推荐使用 useRef 配合 useEffect
  • 动态属性更新:你在副作用中需要读取最新的 DOM 属性(如滚动位置),但这些属性并不由 React 控制,所以每次渲染后都需要重新读取。

不过,现代 React 已经提供了如 useLayoutEffect 或直接在事件处理中处理的更好方案,所以故意不传依赖项的例子非常罕见

6. 使用 ESLint 规则保护自己

React 官方强烈推荐配置 eslint-plugin-react-hooks,其中的 exhaustive-deps 规则会检查 useEffect 的依赖项是否完整。

  • 如果你不传第二个参数,但回调中使用了任何外部变量(state、props),ESLint 会发出警告,提醒你可能需要添加依赖数组。
  • 如果你确实需要每次渲染都执行,且确认不会造成问题,可以临时禁用该行规则,但务必添加注释解释原因。
useEffect(() => {
  // 确实需要每次渲染后执行,且不会在这里更新 state
  // eslint-disable-next-line react-hooks/exhaustive-deps
  analytics.logRender();
});

7. 实际情况演示:对比实验

假设有一个显示当前时间的组件:

function CurrentTime() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());

  // ❌ 不传第二个参数:每次渲染后都会重新设置定时器
  // 导致定时器重复创建,且时间疯狂刷新
  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    // 没有清理函数也会导致内存泄漏
  });

  return <div>{time}</div>;
}

修正的方式是传入空数组 [],让定时器只在挂载时创建,并返回清理函数在卸载时销毁:

useEffect(() => {
  const timer = setInterval(() => {
    setTime(new Date().toLocaleTimeString());
  }, 1000);
  return () => clearInterval(timer);
}, []); // 👈 关键:传入空数组

8. 总结

  • 不传第二个参数 ≈ 每次渲染后无条件执行。
  • 这很容易导致无限循环,尤其在副作用中更新 state 时。
  • 绝不要把它当作“默认模式”——如果你不确定依赖项是什么,至少应该传入空数组 [],或者明确列出所有依赖。
  • 使用 ESLint 规则辅助检查,保持代码的可预测性和性能。

记住:useEffect 的第二个参数不是可选项,而是一种控制机制。省略它,就是将控制权完全交给 React 的每次渲染,代价往往是你不想要的。