← 返回题目列表

useEffect 的执行时机和依赖数组怎么理解?

高频 中等 第 12 / 27 题 更新于 2026/07/27
ReactHooksuseEffect副作用

简化版

useEffect 用来把渲染结果同步到外部系统,通常在提交后执行。依赖数组决定 effect 什么时候重新执行:不传则每次提交后执行,空数组按生产语义在挂载后执行,传依赖则依赖变化后执行;开发 StrictMode 还会额外做一次 setup/cleanup 探测。

详细版

常见写法:

useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);

依赖含义:

  • 不传依赖:每次 render 后执行。
  • []:组件挂载后执行,卸载时 cleanup。
  • [a, b]:a 或 b 变化后执行。

cleanup 会在组件卸载前执行,也会在下一次 effect 执行前先清理上一次 effect。

完整版教学

一、Effect 是渲染结果的副作用

React 函数组件本身应该是纯渲染逻辑:输入 props/state,返回 UI。发请求、订阅事件、操作 DOM、启动定时器,都不属于纯渲染,所以放到 effect。

effect 的思路不是模拟生命周期,而是描述“这个渲染结果需要同步到外部系统什么东西”。

二、依赖数组不是性能开关

很多人把依赖数组当成“控制执行次数”的技巧,这是危险的。依赖数组表达 effect 用到了哪些响应式值。漏依赖会导致闭包拿到旧值,形成 stale closure。

如果 effect 里用了某个 props 或 state,通常就应该放进依赖数组。eslint-plugin-react-hooks 的 exhaustive-deps 规则就是为了帮你发现这类问题。

三、cleanup 的重要性

订阅、定时器、请求监听等副作用必须清理:

useEffect(() => {
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, [onResize]);

否则组件卸载后回调仍然存在,可能造成内存泄漏或更新已卸载组件。

清理时必须使用与注册时相同的事件目标、事件名和函数引用;在 add/remove 两处各写一个新匿名函数并不能移除旧监听。

四、面试追问与工程落地

常见追问是“为什么 StrictMode 下 effect 执行两次”。开发环境 StrictMode 会额外挂载、清理、再挂载,用来暴露副作用不幂等的问题。生产环境不会这样,但你的 effect 应该能经受重复执行。

还会问“请求放 useEffect 需要注意什么”。要处理竞态和取消。组件卸载或参数变化后,旧请求可能晚回来覆盖新状态,可以用 AbortController 或 ignore flag 清理。

工程里如果一个 effect 只是为了从已有 state 派生另一个 state,通常应该改成计算值,而不是 effect。能在 render 里算出来的,就不要放 effect。

五、依赖比较与 setup/cleanup 时间线

依赖数组必须是固定长度并内联书写,React 对每一项使用 Object.is 与上次提交的值比较。只要有一项变化,下一轮 effect 会先用旧 props/state 执行上一次 cleanup,再用新值执行 setup;卸载时再做最后一次 cleanup。

首次提交:                 setup(room=A)
room A → B 的提交:cleanup(room=A) → setup(room=B)
组件卸载:            cleanup(room=B)
StrictMode 开发探测:setup → cleanup → setup(额外一轮)

假设 roomId 在 1 秒内依次提交 A、B、C 三个值,正确的连接数量应始终最多为 1:连接 A 前无旧连接,切到 B 先断 A,切到 C 先断 B。若 cleanup 缺失,最终会残留 3 条连接,重复收消息和资源泄漏都会出现。

依赖写法触发语义常见风险
省略第二参数每次提交后无条件 setState 易形成循环
[]挂载 setup、卸载 cleanup读取变化值会形成旧闭包
[a, b]任一依赖变化新对象/函数导致过度重连
漏写依赖表面少执行逻辑与最新渲染脱节

六、竞态、服务端和“你可能不需要 Effect”

Effect 只在客户端执行,不参与服务端渲染。直接在 Effect 中取数还要处理缓存、瀑布请求、取消和竞态;参数变化后旧请求可能更晚返回,可以用 AbortController 取消,或用 cleanup 中的 ignore 标记丢弃旧响应。

若只是根据 firstNamelastName 计算 fullName,直接在 render 中计算即可;先 render 旧值,再由 effect setState 会制造额外提交。用户点击产生的提交也应放事件处理器,因为 effect 只知道“某次渲染已提交”,不知道哪个具体交互意图触发它。

Effect 的定义是“让 React 状态与外部系统同步”,不是“渲染后可以随便放代码的抽屉”。

七、常见误区与追问

  • 误区:空依赖数组能安全地冻结任意 effect。 若 setup 读取会变化的 props/state,空数组只会保留首次渲染闭包。
  • 误区:cleanup 只在组件卸载时执行。 依赖变化后的下一次 setup 前,也会先清理上一轮资源。
  • 误区:StrictMode 生产环境会永久执行两次请求。 额外 setup/cleanup 是开发探测,真正问题是 effect 没有正确清理或幂等。
  • 追问:依赖比较使用深比较吗? 不使用,React 对每项执行 Object.is,新建对象和函数通常会被判为变化。
  • 追问:怎样消除不必要的函数依赖? 可把只供 effect 使用的函数移进 setup,或把非响应式逻辑移到组件外,而不是删除真实依赖。
  • 追问:请求竞态怎样处理? 取消旧请求或在 cleanup 标记旧响应失效,只允许当前参数对应结果落地。
  • 追问:什么时候根本不需要 effect? 纯派生值、事件专属逻辑和可在 render 中完成的同步计算通常不需要。

八、加强记忆

useEffect 不是生命周期替身,而是“把渲染结果同步到外部世界”。依赖数组要诚实,cleanup 要成对,副作用要能重复执行和清理。

按 setup→旧 cleanup→新 setup 的过程思考,纯派生计算和用户事件则不要绕进 Effect。