【图】React源码解析-从源码数据结构、依赖算法、调度流水线到内存管理,深挖useEffect的全部核心机制

useEffect 在 Fiber 上的数据结构(Hook 对象与副作用链表)

这张图展示 useEffect 在首次挂载时,如何在 Fiber 节点上构建 Hook 对象,以及副作用(Effect)如何被挂载到更新队列(updateQueue)中。

📝 源码级深度解析(数据结构)

  1. 双重存储机制useEffect 产生的副作用信息实际上被存储在两个地方。一是作为 Hook 节点 的一部分(hook.memoizedState),供函数组件在更新时读取依赖项;二是被 复制一份 推入 Fiber.updateQueue 的副作用链表中,供 Commit 阶段遍历执行。这种设计使得 Render 阶段和 Commit 阶段的数据流解耦。
  2. Effect 对象的字段 :每个 Effect 对象包含 create(执行函数)、deps(依赖数组)、destroy(清理函数,初始为 null)和 next(指向下一个 Effect 的指针)。tag 属性(如 PassiveLayout)决定了该副作用在 Commit 的哪个子阶段被执行。
  3. tag 的位运算标记 :Fiber 节点的 flags(副作用标识)使用位掩码。当挂载 useEffect 时,React 会执行 fiber.flags |= PassiveEffect。这使得 React 在提交阶段能快速判断:是否需要在 Mutation 子阶段结束后,去调度 flushPassiveEffects

依赖项比较机制(areHookInputsEqual)------是否跳过执行

这张图剖析 useEffect 更新时,React 如何比对前后两次的依赖数组,决定是否调用副作用函数。

📝 源码级深度解析(依赖比较)

  1. 比较算法 Object.is :React 使用 Object.is 进行依赖项的相等性判断,而不是 === 或浅比较(Shallow Compare)。Object.is=== 的区别在于:+0 不等于 -0,且 NaN 等于自身。这是大多数开发者容易忽略的细节。
  2. 浅层比较(Shallow Comparison)areHookInputsEqual 实现的是浅层遍历for 循环逐项比对),而非深拷贝比对。如果依赖项是对象或数组,即使内容不变但引用地址变了,React 依然会判定为"变化",从而触发副作用重新执行。这也是为什么在 useEffect 依赖项中传递对象字面量(如 {id: 1})容易导致无限循环的原因。
  3. 依赖项为空数组 [] 的特殊性 :当 deps 为空数组时,首次挂载后,areHookInputsEqual 比较时发现新旧 deps 长度均为 0,循环自然结束且无差异。因此,该 Effect 仅在首次挂载时执行一次,后续更新全部跳过。

useEffect 的异步调度与执行时机(Passive 阶段)

这张图展示 useEffect 从 Commit 阶段到浏览器绘制,再到空闲期执行的完整时序流水线。

📝 源码级深度解析(调度时机)

  1. Mutation 子阶段只负责"调度"而非"执行"useEffectcreate 函数绝对不在 Mutation 子阶段执行。React 在这个阶段仅仅执行 scheduleCallback(NormalPriority, flushPassiveEffects)(向 Scheduler 发起异步调度请求),将执行权交给浏览器的绘制流水线。
  2. Layout 子阶段与 useLayoutEffect 的边界Layout 子阶段是同步且阻塞绘制的(发生在 Mutation 之后、Paint 之前)。useLayoutEffect 在此阶段同步执行,因此可以安全读取 DOM 布局属性(如 getBoundingClientRect),但耗时操作会直接导致掉帧。
  3. 延迟执行的权衡(Trade-off) :将 useEffect 推迟到绘制后执行,确保了数据请求、日志记录、非关键 DOM 操作不会延迟用户看到屏幕内容的时机。这也是 React 官方推荐将绝大多数副作用放在 useEffect 而不是 useLayoutEffect 中的原因。

清理闭环(Cleanup)与组件卸载时的收尾流程

这张图剖析 useEffect 的清理函数(destroy)在依赖变更时组件卸载时的完整执行逻辑,以及如何防止内存泄漏。

📝 源码级深度解析(清理闭环)

  1. 清理函数的存储链路destroy 函数并非存储在 Hook 节点的 memoizedState 中,而是附着在 Effect 对象本身effect.destroy)。每次 create 执行后,其返回值(清理函数)被赋值给 effect.destroy。这使得 React 在卸载或更新前,能够精准定位到该 Effect 对应的清理逻辑。
  2. 更新时的"先清理后创建"顺序 :当依赖项变化时,React 严格遵循 "先执行旧的 destroy,再执行新的 create" 顺序。这确保了旧的监听器(如 WebSocket 连接、DOM 事件绑定)在建立新连接之前被彻底移除,避免重复订阅导致的内存泄漏。
  3. 卸载时的优先级执行 :组件卸载时,useEffect 的清理函数会在 Mutation 子阶段 (DOM 移除之前或同时)同步执行。这是为了防止在组件从视图中移除后,异步清理函数尚未执行而导致"无法访问已卸载组件的状态"的 React 警告。如果清理函数中存在异步操作(如 setTimeout),React 无法保证其完成时机,因此官方推荐清理函数保持同步简单逻辑。
  4. React 19 的编译器优化(React Compiler) :在未来的 React 19 + Compiler 版本中,如果 useEffect 内部没有引用任何响应式变量,编译器可能自动省略依赖项比较或将其提升为静态常量,进一步提升性能。但清理闭环的核心机制将保持兼容。

useEffect 的完整工作流

维度 核心结论
数据结构存储 useEffect 同时存在于 Hook 链表(用于读依赖)和 updateQueue(用于执行),通过 Passive 标记区分。
依赖比较与跳过 使用 Object.is 逐项浅比较,空数组代表永不更新,对象/数组引用变化会触发重执行。
异步调度时机 Mutation 阶段仅发起调度,Layout 阶段执行 useLayoutEffect,绘制后空闲期才执行 useEffect
清理闭环机制 更新前先执行旧 destroy,再执行新 create;卸载时在 Mutation 阶段同步执行 destroy 释放资源。
相关推荐
用户298698530142 小时前
在 React 中通过 JavaScript 实现 Excel 单元格、行与列的锁定保护
javascript·react.js·excel
光影少年18 小时前
RN 样式特点:Flex 布局默认、没有像素单位、样式不能级联
react native·react.js·掘金·金石计划
小蚂蚁i21 小时前
React Hooks 原理深度解析:从会用到真正懂
前端·react.js
索西引擎1 天前
【React】虚拟 DOM:原理、机制与核心价值分析
前端·react.js·前端框架
光影少年1 天前
react的View/Text/Image/ScrollView 常用组件注意事项
开发语言·前端·javascript·react.js·前端框架
索西引擎1 天前
【React】React Fiber 架构:设计动机、核心机制与调度策略分析
前端·react.js·架构
索西引擎1 天前
【React】JSX:JavaScript 语法扩展的本质、编译机制与表达能力分析
前端·javascript·react.js
hxy06012 天前
React-Call让你的React组件像异步函数一样调用
前端·react.js