useEffect依赖数组写空数组和完全不写有什么区别?

作者:函数茶馆

时间:2026-11-13

浏览:616

全面解析React中useEffect完全不写依赖项与传入空数组[]的核心区别。从执行时机、生命周期映射、渲染时间线、死循环陷阱与闭包状态陈旧等维度展开深度对比,并给出可落地的最佳工程选型指南。

文章目录
在 React 中,useEffect 完全不写依赖项与传入空数组 [] 的核心区别在于执行频率与触发时机:完全不写依赖项时,副作用函数在初次挂载以及随后的每一次重新渲染后都会执行;而传入空数组 [] 时,副作用函数仅在组件首次挂载完成后执行一次,后续的重新渲染均会被跳过。理解这一区别是避免死循环与闭包陈旧状态的关键。
useEffect依赖数组写空数组和完全不写有什么区别?
图 1:useEffect 依赖项核心差异对照概览

一、核心机制:两者底层运行规则的区别

在函数组件内部,React 在每次渲染时都会调用组件函数本身。当 React 执行到 useEffect 调用时,它会对比当前传入的“依赖项数组”与“上一次渲染时记录的依赖项数组”。两者的比较行为遵循以下明确规则:

  • 完全不传递第二个参数(即无依赖数组):React 不进行任何依赖比较。这意味着 React 默认该副作用与组件的每一次渲染结果都相关。只要组件发生了重新渲染(无论是自身 state 改变、父组件重新渲染还是 Context 发生变动),DOM 绘制完成后,该 Effect 就会无条件执行。
  • 传递空数组 [] 作为第二个参数:依赖数组存在,且长度为 0。React 在后续每次渲染时都会比较依赖项,因为数组始终为空、不存在任何可变依赖,所以比较结果永远是“依赖项未发生任何变化”。因此,除了初次挂载(Mounting)阶段外,后续所有更新渲染阶段 React 都会直接跳过该 Effect。
对比维度 完全不传依赖数组:useEffect(fn) 传递空数组:useEffect(fn, [])
首次挂载执行 执行 1 次 执行 1 次
组件重新渲染执行 每次渲染后都会执行 完全跳过,不再执行
清理函数(cleanup)时机 在下一次 Effect 执行之前,以及组件卸载时 仅在组件彻底从屏幕卸载(Unmount)时
典型应用场景 DOM 尺寸测量、与外部无条件持续同步 单次初始化请求、挂载全局事件监听、建立 WebSocket
最容易踩中的 Bug 内部无条件触发 setState 导致无限循环渲染 捕获初次渲染的局部变量形成闭包陈旧值

二、深入执行时机:组件渲染生命周期与时间线

要精准掌控两者的差异,需要结合组件的生命周期(挂载、多次重渲染、卸载)以及清理函数(Cleanup Function)的执行顺序来观察。

useEffect依赖数组写空数组和完全不写有什么区别?
图 2:组件生命周期中两种写法的执行与清理时序对比

1. 不写依赖项的时序表现

当编写如下代码时:

useEffect(() => {
  console.log("Effect 执行");
  return () => {
    console.log("清理函数 执行");
  };
});

在组件的真实运行轨迹中:

  1. 组件挂载:执行渲染得到 DOM → 打印 "Effect 执行"。
  2. 触发第 1 次更新:执行渲染得到新 DOM → 打印 "清理函数 执行"(清理上一次的副作用) → 打印 "Effect 执行"(执行本次副作用)。
  3. 触发第 2 次更新:再次先执行上一轮的清理函数,紧接着执行新一轮的副作用。
  4. 组件卸载:最终打印一次 "清理函数 执行"。

这意味着,只要组件在频繁更新,副作用和清理函数就会如影随形地成对高频触发。

2. 传空数组时的时序表现

而如果将代码改写为包含空数组:

useEffect(() => {
  console.log("挂载完成,仅执行一次");
  return () => {
    console.log("组件销毁,仅清理一次");
  };
}, []);

在整个生命周期中,中间无论经历多少次父组件传参变化或内部其他状态更新,控制台都不会有任何多余输出,直到组件从页面中卸载,才会执行一次返回的清理函数。

三、常见故障与陷阱:不写依赖导致的“无限死循环”

在初学 React 时,最容易造成页面卡死或控制台狂喷报错的错误之一,就是在没有传入依赖数组的 useEffect 内部直接调用了状态更新函数(如 setState)。

useEffect依赖数组写空数组和完全不写有什么区别?
图 3:不写依赖项引发无限循环渲染的因果链条与排查路径

故障代码复现

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

  // 错误示范:没有依赖项,且在内部调用了 setState
  useEffect(() => {
    setCount(count + 1); // 触发重新渲染!
  });

  return <div>当前计数:{count}</div>;
}
⚠️ 报错信息:Maximum update depth exceeded

React 提示“超过最大更新深度”。因为:渲染组件 → 触发 Effect → 执行 setCount → 触发重新渲染 → 再次触发 Effect → 再次执行 setCount……无限循环直至浏览器崩溃或被 React 保护机制强制中断。

解决与排查方法

  • 如果只需要初始化执行:补全第二个参数为 []。例如页面打开时请求用户基本信息,请求回来后保存到 state 中,必须给该 Effect 传递空数组或依赖项。
  • 如果是响应某个参数改变:明确写入具体依赖,例如 [userId],且内部应具备终止条件,避免无休止调用。
  • 如果只是数据计算:不需要写在 Effect 内部。如果某个值完全可以通过现有 props 或 state 换算得出,直接在组件函数体内派生即可(例如 const fullName = firstName + ' ' + lastName;)。

四、空数组的隐藏风险:闭包陷阱(Stale Closure)

虽然空数组避免了每次重渲染都执行的高频开销和死循环,但由于它“永不重新执行”,如果副作用内部引用了组件内的 props 或 state,就会悄然踩入闭包陈旧值陷阱。

useEffect依赖数组写空数组和完全不写有什么区别?
图 4:空数组引起的闭包状态陈旧与函数式更新解决方案

典型踩坑:自增定时器永远卡在 1

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

  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的 count 捕获的是组件挂载时的初始快照,即 count = 0
      setCount(count + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空数组意味着定时器回调里的闭包永远不会刷新!

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

读者在运行这段代码时会发现:数字从 0 变成 1 之后,就再也不会增加了。

根本原因:JavaScript 的函数闭包在创建那一刻记录了作用域内的变量。因为依赖数组是 [],React 永远不会重新创建这个 Effect 函数,所以定时器内部每次执行时,访问到的 count 永远是首次渲染时的初始值 0。每一次定时器触发,都在计算 0 + 1,因此状态被不断重置为 1。

正确修复方案

针对闭包陈旧问题,主要有两种解法:

  1. 使用函数式更新(推荐):将 setCount(count + 1) 改为 setCount(prev => prev + 1)。这样回调不再依赖外部的 count 变量,React 会在执行更新时自动将最新的状态值作为参数传入。
  2. 诚实填写真实依赖:若副作用必须依赖该变量,将其加入依赖数组 [count],并在清理函数中清除定时器。这样每次 count 变化都会重置定时器并拉取最新值。

五、工程最佳实践与选型指南

在实际工程开发中,开发者应当如何在这几种写法之间做出抉择?我们可以借助下面的决策路径快速做出判断。

useEffect依赖数组写空数组和完全不写有什么区别?
图 5:useEffect 依赖项选择决策树

1. 什么时候该完全不写依赖项?

在绝大多数业务逻辑中,完全不写依赖项是非常少见的,甚至往往属于编码失误。但它确实存在合法的使用场景:

  • DOM 几何位置追踪与测量:每次渲染可能引发 DOM 节点的重排或尺寸变动,需要在每次绘制后同步测量某些容器的实际高度并记录(若需要同步阻断渲染,可优先考虑 useLayoutEffect)。
  • 第三方非受控组件同步:某些无法完全由 React 受控的原生库,需要在组件发生任何渲染变动时进行无差别重绘同步。

2. 什么时候应该写空数组 []?

  • 全局事件监听与解绑:例如在页面挂载时通过 window.addEventListener('resize', handleResize) 监听窗口大小,并在卸载时执行清理。
  • 长连接与第三方 SDK 初始化:例如与服务器建立 WebSocket 连接、初始化图表实例,并在销毁时断开连接。
  • 仅在页面加载时拉取一次首屏数据:确保异步请求只发一次,避免随着父组件渲染而产生雪崩式的重复接口调用。

3. 核心黄金法则:听从 ESLint 依赖项规则

现代 React 开发推崇的核心原则是:不要凭主观臆断去“骗”依赖数组。 官方提供的 eslint-plugin-react-hooks 中的 exhaustive-deps 规则能够精准静态扫描 Effect 内部引用的所有局部变量。凡是在 Effect 内部使用到的组件内变量(props、state 或局部函数),都应该如实地声明在依赖数组中;如果你认为它不应该触发重新执行,应当通过调整逻辑(如使用 useCallback、函数式更新、或将函数移出组件)来从根源解决,而不是强行写一个 [] 来掩盖问题。

总结

回顾核心问题:useEffect 不写依赖数组时,是“每次渲染后的无条件全量同步”;而写入空数组 [] 时,是“仅在初次挂载时的单次同步”。在编写 React 代码时,务必牢记:不写依赖项要警惕内部更新引发的死循环,写空数组要警惕捕获初态的闭包陷阱。清晰地把握这两者的区别与成立条件,是写出稳定、高效 React 组件的坚实基础。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章