- Published on
useEffect 内部原理
useEffect 是 React 函数组件中处理副作用 (Side Effects) 的核心 Hook。尽管其 API 简洁直观,但其内部机制是一个高度精密的流程,旨在确保副作用在正确的时机执行,同时避免阻塞关键渲染路径 (Critical Rendering Path)。
useEffect 的数据结构与创建
useEffect 的内部工作,核心围绕着一个名为 Effect 的数据结构展开。
Effect 对象的数据结构
Effect 对象是 React 用来存储和管理副作用相关信息的内部数据结构。它包含以下核心属性:
tag: 一个位掩码 (bitmask),用于标记副作用的类型。HookHasEffect标记了该副作用是否需要在提交阶段被执行。create: 开发者传入useEffect的副作用回调函数,即() => { ... }。destroy:create函数可选返回的清理函数。deps: 开发者传入的依赖数组。next: 一个指针,用于将多个Effect对象链接成一个环形链表。
初始挂载阶段 (Mount Phase)
在组件的初始挂载 (initial mount) 阶段,useEffect 的主要工作是创建并存储 Effect 对象,以备后续执行。
- React 会在当前 Fiber 节点的
memoizedState链表上创建一个 Hook 节点,并为当前useEffect调用在链表中分配一个固定位置,用于记录 Hook 的状态和顺序。 useEffect内部会调用pushEffect(),根据开发者提供的create回调和依赖数组 (deps) 创建一个 Effect 对象。- 这个 Effect 对象会被标记为
HookHasEffect,表示它需要在提交阶段执行副作用逻辑。 - Effect 对象随后会被加入到当前 Fiber 节点的
updateQueue(副作用队列)中,形成一个环形链表,以便在提交阶段依次执行。
更新阶段:依赖对比与副作用标记
在组件的重渲染 (re-render) 阶段,useEffect 不再是简单地创建对象,而是会根据依赖数组的变化,来决定是否需要执行副作用。
依赖对比的原理
useEffect 内部会通过一个名为 areHookInputsEqual() 的函数来比较新旧依赖数组。
副作用的条件标记
useEffect 的精妙之处在于它利用了依赖数组的比较结果,来决定是否为 Effect 对象添加 HookHasEffect 标记。
- 依赖未变化: 如果
areHookInputsEqual()返回true,useEffect会重新创建一个Effect对象,但不会为其添加HookHasEffect标记。这意味着,这个副作用不会在本次提交阶段被执行。 - 依赖已变化或无旧 Hook: 如果依赖数组变化,
useEffect会创建一个新的Effect对象,并为其添加HookHasEffect标记。这个标记是副作用在提交阶段得以执行的“通行证”。
提交阶段:副作用的执行与清理
useEffect 的回调函数本身不会在渲染阶段执行。它们被安排在提交阶段,且被调度为异步的、不会阻塞浏览器绘制的“被动副作用 (Passive Effects)”。
执行顺序:先子后父的清理与挂载
useEffect 的执行遵循一个严格的层级顺序,以保证逻辑的正确性:
- 副作用的清理 (Cleanup): 在执行新副作用之前,React 会首先遍历 Fiber 树,从子组件开始,向上到父组件,依次执行所有需要清理的
Effect对象的destroy函数。 - 副作用的挂载 (Mount): 清理完成后,React 会再次遍历 Fiber 树,从子组件开始,向上到父组件,依次执行所有需要挂载的
Effect对象的create函数。
这个流程确保了父组件的副作用总能依赖于子组件的副作用执行完毕后(如果存在父子依赖),再进行自身的清理和挂载。
阶段时机与调度
useEffect 的执行流程与 DOM 更新流程是解耦的。
- DOM 更新:在提交阶段,React 首先同步执行所有 DOM 变更,并执行
useLayoutEffect等同步 effect。 - 浏览器绘制: 浏览器在同步 DOM 变更后,会进行绘制,将更改呈现到屏幕上。
useEffect执行: 绘制完成后,React 异步地调度flushPassiveEffects来执行useEffect的清理和挂载回调。这个异步的调度确保了useEffect永远不会阻塞浏览器的绘制,从而保证了界面的流畅性。