React中 useEffect和useLayoutEffect源码原理

核心概念:副作用与 Hooks

在 React 函数组件中,诸如数据获取、订阅、手动更改 DOM 等操作被称为"副作用"(Side Effects),因为它们会影响组件之外的东西,并且在渲染过程中无法完成。React Hooks(如 useState, useEffect)允许我们在函数组件中使用 state 和其他 React 特性。

useEffectuseLayoutEffect 就是专门用来处理副作用的 Hooks。它们的主要区别在于执行时机

一、useEffect:异步执行的副作用

用途:处理不需要阻塞浏览器绘制的副作用,如数据获取、设置订阅、手动操作非关键 DOM 等。它是最常用的副作用 Hook。

执行时机

  1. 首次渲染后 :在组件完成渲染并且浏览器完成绘制(Paint)之后异步执行。
  2. 更新渲染后 :在组件更新渲染完成并且浏览器完成绘制之后异步执行(如果依赖项发生变化)。
  3. 卸载前:执行上一次 effect 返回的清理函数(cleanup function)。

核心原理与源码分析

  1. 调度(Scheduling)阶段 - Render Phase

    • 当你调用 useEffect(create, deps) 时,React 内部会通过当前的 Dispatcher 调用相应的 Hook 实现(例如 useEffect 对应的 mountEffectupdateEffect)。
    • 关键文件react-reconciler/src/ReactFiberHooks.js
    • 核心逻辑 (mountEffect / updateEffect):
      • 创建 Effect 对象 :React 会创建一个包含 create 函数(你的 effect 函数)、destroy 函数(上一次 effect 返回的清理函数)、依赖项 deps 以及 tag(标记 effect 类型)的对象。

      • 标记 Effect 类型 :对于 useEffect,它会被打上 Passive 标签。这个标签很重要,决定了它在 Commit 阶段的执行时机。Passive 意味着它不会阻塞浏览器绘制。

        javascript 复制代码
        // react-reconciler/src/ReactFiberHooks.js (简化示意)
        
        // mountEffect / updateEffect 内部逻辑类似
        function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
          const hook = mountWorkInProgressHook(); // 获取当前 Hook 状态
          const nextDeps = deps === undefined ? null : deps;
          // ... (省略部分检查逻辑)
        
          // 重点:给 Fiber 节点打上 PassiveEffect 标记
          // PassiveEffect = Passive | HookHasEffect
          // Passive 来自 ReactFiberFlags.js, HookHasEffect 表明这个 hook 有副作用要执行
          fiber.flags |= fiberFlags; // fiberFlags 包含 PassiveEffect
        
          // 将 effect 信息存储在 hook 的 memoizedState 中
          hook.memoizedState = pushEffect(
            HookHasEffect | hookFlags, // hookFlags 区分是 useEffect 还是 useLayoutEffect 等
            create, // 你的 effect 函数
            undefined, // destroy 函数,首次 mount 为 undefined
            nextDeps, // 依赖项
          );
        }
        
        function pushEffect(tag, create, destroy, deps) {
          // 创建 effect 对象
          const effect = {
            tag,
            create,
            destroy,
            deps,
            // Circular reference 的 next 指针,用于形成 effect 链表
            next: null,
          };
          // 将 effect 添加到 Fiber 节点的 updateQueue.lastEffect 链表中
          let componentUpdateQueue = fiber.updateQueue;
          if (componentUpdateQueue === null) {
            componentUpdateQueue = createFunctionComponentUpdateQueue();
            fiber.updateQueue = componentUpdateQueue;
            componentUpdateQueue.lastEffect = effect.next = effect;
          } else {
            const lastEffect = componentUpdateQueue.lastEffect;
            if (lastEffect === null) {
              componentUpdateQueue.lastEffect = effect.next = effect;
            } else {
              const firstEffect = lastEffect.next;
              lastEffect.next = effect;
              effect.next = firstEffect;
              componentUpdateQueue.lastEffect = effect;
            }
          }
          return effect;
        }
      • 依赖项比较 :在 updateEffect 中,会比较 deps 和上一次存储的依赖项。只有当依赖项发生变化(或者没有提供依赖项数组)时,才会标记 HookHasEffect,表示需要执行新的 effect。

  2. 提交(Commit)阶段

    • React 完成 DOM 更新后,会处理带有副作用标记(Flags)的 Fiber 节点。
    • 关键文件react-reconciler/src/ReactFiberCommitWork.js
    • 核心逻辑
      • Commit 阶段分为几个子阶段,useEffect 的执行发生在 commitPassiveEffects 这个子阶段。

      • 异步调度commitPassiveEffects 不会立即执行 effect。它使用 React 的调度器(Scheduler)来异步 调度 effect 的执行。这意味着浏览器可以先完成绘制,然后再执行这些 effect。

        javascript 复制代码
        // react-reconciler/src/ReactFiberCommitWork.js (简化示意)
        
        // commitRootImpl 函数是 Commit 阶段的入口
        function commitRootImpl(root, ...) {
          // ... (DOM Mutations 等操作)
        
          // 标记需要执行 Passive Effects
          root.effect_tag |= PassiveEffects;
        
          // ...
        
          // 在 Commit 阶段的末尾,调度 Passive Effects 的执行
          // flushPassiveEffects 会使用 Scheduler 来异步执行
          if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
              (finishedWork.flags & PassiveMask) !== NoFlags) {
            if (!rootDoesHavePassiveEffects) {
              rootDoesHavePassiveEffects = true;
              // 使用 Scheduler 调度 flushPassiveEffects 的执行
              scheduleCallback(NormalPriority, () => {
                flushPassiveEffects();
                return null;
              });
            }
          }
          // ...
        }
        
        // flushPassiveEffects 会遍历 Fiber 树,执行 Passive Effects
        function flushPassiveEffects() {
          // ... (省略状态检查和循环逻辑)
          commitPassiveUnmountEffects(); // 执行清理函数
          commitPassiveMountEffects();  // 执行 effect 函数
          // ...
        }
        
        // commitPassiveMountEffects 遍历 Fiber 树,找到带 Passive 标记的节点
        function commitPassiveMountEffects(root, finishedWork) {
          // 遍历 Fiber 树,找到带有 Passive 标记的节点
          // ...
          commitPassiveMountEffects_complete(subtreeRoot, root, finishedWork);
          // ...
        }
        
        // 真正执行 effect 的地方
        function commitPassiveMountEffects_complete(...) {
           // ... 遍历 Fiber 节点
           const effect = node.updateQueue.lastEffect;
           if (effect !== null) {
             const firstEffect = effect.next;
             let currentEffect = firstEffect;
             do {
               // 检查 tag 是否包含 Passive 和 HookHasEffect
               if ((currentEffect.tag & HookPassive) !== NoHookEffect &&
                   (currentEffect.tag & HookHasEffect) !== NoHookEffect) {
                 // 执行 effect 的 create 函数
                 const create = currentEffect.create;
                 currentEffect.destroy = create(); // 保存返回的 cleanup 函数
               }
               currentEffect = currentEffect.next;
             } while (currentEffect !== firstEffect);
           }
           // ...
        }
        
        // commitPassiveUnmountEffects 类似,但执行的是 destroy 函数
        function commitPassiveUnmountEffects(...) {
          // ... 遍历 Fiber 节点
          // ... 找到需要 unmount 的 effect
          // ... 执行 effect.destroy()
          // ...
        }
      • 执行流程

        1. flushPassiveEffects 首先调用 commitPassiveUnmountEffects,遍历 Fiber 树,执行那些在上一次渲染中被标记为需要清理(因为组件卸载或依赖项变化)的 useEffectdestroy 函数。
        2. 然后调用 commitPassiveMountEffects,遍历 Fiber 树,执行本次渲染中被标记为需要执行的 useEffectcreate 函数,并将其返回的清理函数(如果有)存储在 effect.destroy 中,供下次清理使用。

小结useEffect 通过 Passive 标记和异步调度,确保了副作用的执行不会阻塞浏览器渲染,提供了更好的用户体验,尤其适用于那些不需要立即反映在视觉上的操作。

二、useLayoutEffect:同步执行的副作用

用途 :处理需要在浏览器绘制之前同步执行的副作用。常见场景包括:

  • 读取 DOM 布局(如获取元素尺寸、位置)。
  • 同步修改 DOM 并希望用户看不到中间状态(避免闪烁)。

执行时机

  1. 首次渲染后 :在 React 完成所有 DOM 变更之后,但在浏览器绘制之前同步执行。
  2. 更新渲染后 :在 React 完成所有 DOM 变更之后,但在浏览器绘制之前同步执行(如果依赖项发生变化)。
  3. 卸载前:执行上一次 effect 返回的清理函数。

核心原理与源码分析

  1. 调度(Scheduling)阶段 - Render Phase

    • 调用 useLayoutEffect(create, deps)
    • 关键文件react-reconciler/src/ReactFiberHooks.js
    • 核心逻辑 (mountLayoutEffect / updateLayoutEffect):
      • useEffect 非常相似,也会创建 Effect 对象并存入 updateQueue

      • 关键区别useLayoutEffect 给 Fiber 节点打上的标记不同。它通常会打上 UpdateLayout 相关的标记(具体标记可能随 React 版本演变,但核心是非 Passive 的 Effect 标记)。这些标记表明 effect 需要在 Layout 阶段同步执行。

        javascript 复制代码
        // react-reconciler/src/ReactFiberHooks.js (简化示意)
        
        function mountLayoutEffect(create, deps) {
          // 注意这里的 fiberFlags 是 Update (或其他同步标记),而不是 Passive
          return mountEffectImpl(Update, HookLayout, create, deps);
        }
        
        function updateLayoutEffect(create, deps) {
          // 注意这里的 fiberFlags 是 Update (或其他同步标记),而不是 Passive
          return updateEffectImpl(Update, HookLayout, create, deps);
        }
        
        // mountEffectImpl / updateEffectImpl 内部逻辑与 useEffect 类似
        // 只是传入的 fiberFlags 和 hookFlags 不同
        // fiber.flags |= Update; // 打上同步执行的标记
        // hook.memoizedState = pushEffect(HookHasEffect | HookLayout, ...);
  2. 提交(Commit)阶段

    • 关键文件react-reconciler/src/ReactFiberCommitWork.js
    • 核心逻辑
      • useLayoutEffect 的执行发生在 commitLayoutEffects 这个子阶段。这个阶段在 DOM Mutations 完成之后,但在浏览器绘制之前。

      • 同步执行 :与 useEffect 不同,commitLayoutEffects同步 执行的。React 会立即遍历 Fiber 树,执行所有标记了 Update / Layout 的 effect。

        javascript 复制代码
        // react-reconciler/src/ReactFiberCommitWork.js (简化示意)
        
        function commitRootImpl(root, ...) {
          // ...
          // 1. 执行 DOM Mutations (before mutation effects)
          // ...
        
          // 2. 执行 Layout Effects (包括 useLayoutEffect 的清理和执行)
          // commitLayoutEffects 是同步执行的
          commitLayoutEffects(root, finishedWork);
        
          // ... (然后才会调度 Passive Effects)
          if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
              (finishedWork.flags & PassiveMask) !== NoFlags) {
             // ... scheduleCallback(NormalPriority, flushPassiveEffects);
          }
        
          // ... (Commit 阶段结束,浏览器可以开始绘制)
        }
        
        // commitLayoutEffects 遍历 Fiber 树,同步执行 Layout Effects
        function commitLayoutEffects(root, committedLanes) {
           // ... 遍历 Fiber 树
           commitLayoutEffects_begin(subtreeRoot, root, committedLanes);
           // ...
        }
        
        function commitLayoutEffects_begin(...) {
          while (nextUnitOfWork !== null) {
            // ...
            // 找到带有 LayoutMask (或 Update) 标记的节点
            if ((node.flags & LayoutMask) !== NoFlags) {
              // ...
              // 执行 Layout Effect 的 destroy (清理) 和 create (执行)
              commitLayoutEffectOnFiber(root, node);
              // ...
            }
            // ...
          }
        }
        
        // 真正执行 Layout Effect 的地方
        function commitLayoutEffectOnFiber(root, finishedWork) {
          const current = finishedWork.alternate; // 上一次的 Fiber
          const flags = finishedWork.flags;
        
          if ((flags & LayoutMask) !== NoFlags) { // 检查 Layout 标记
             // ...
             // 执行 unmount (清理)
             commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork, current);
             // 执行 mount (执行)
             commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
             // ...
          }
        }
        
        // commitHookEffectListUnmount 和 commitHookEffectListMount
        // 内部会遍历 hook 的 effect 链表,执行 destroy 或 create 函数
        // 与 commitPassiveMountEffects / commitPassiveUnmountEffects 类似
        // 但它们是同步执行的
      • 执行流程

        1. commitLayoutEffects 会遍历 Fiber 树。
        2. 对于带有 Layout / Update 标记的 Fiber 节点,它会先执行上一次 useLayoutEffect 返回的清理函数(destroy)。
        3. 然后立即执行本次 useLayoutEffectcreate 函数,并将返回的清理函数存起来。
        4. 这个过程是完全同步的,会阻塞主线程,直到所有 useLayoutEffect 执行完毕,浏览器才能进行绘制。

小结useLayoutEffect 通过同步执行机制,保证了在浏览器绘制前完成 DOM 读取和修改,适用于需要精确控制布局或避免视觉闪烁的场景。但因为它会阻塞渲染,应谨慎使用,优先考虑 useEffect

三、useEffect vs useLayoutEffect 核心差异总结

特性 useEffect useLayoutEffect
执行时机 渲染完成 + 浏览器绘制之后异步执行 渲染完成 + DOM 更新之后 ,浏览器绘制之前同步执行
阻塞渲染 ,不阻塞浏览器绘制 ,会阻塞浏览器绘制
适用场景 数据获取、订阅、非关键 DOM 操作等大多数副作用 读取 DOM 布局、同步 DOM 修改、避免闪烁
性能影响 性能较好,不影响首次绘制 可能影响性能,阻塞渲染
内部标记 Passive Effect Flag Update / Layout Effect Flag
调度方式 通过 Scheduler 异步调度 在 Commit 阶段同步执行

四、源码中的关键概念回顾

  • Fiber Node: React 内部表示组件实例、DOM 节点等的数据结构,它承载了组件的状态、props、effect 列表、flags 等信息。
  • Dispatcher : (ReactCurrentDispatcher.current) 一个全局对象,指向当前环境(渲染、mount、update)下应该使用的 Hooks 实现。
  • Hook Object : 存储单个 Hook 状态(如 useState 的 state,useEffect 的 effect 信息)的数据结构,形成链表挂载在 Fiber 节点上。
  • UpdateQueue : 挂载在 Fiber 节点上的队列,用于存储状态更新、effect 列表等。对于 effect,它通常包含一个指向 effect 链表头尾的指针 (lastEffect)。
  • Effect Object : 包含 createdestroydepstagnext 指针的对象,代表一个待处理的副作用。
  • Effect Tag / Flags : (fiber.flags, effect.tag) Fiber 节点和 Effect 对象上的位标记,用于指示需要执行的操作类型(如 DOM 更新、Passive Effect、Layout Effect)和 effect 的具体类型。
  • Commit Phase Sub-stages: Commit 阶段被细分为不同子阶段(如 Mutations、Layout Effects、Passive Effects),以确保不同类型副作用在正确的时机执行。
  • Scheduler: React 的并发调度器,用于安排任务(如渲染、Passive Effects)的优先级和执行时机,实现时间分片和异步执行。
相关推荐
不懂英语的程序猿几秒前
【SF顺丰】顺丰开放平台API对接(注册、API测试篇)
前端·后端
前端九哥4 分钟前
🚀 新一代图片格式 AVIF,对比 WebP/JPEG 有多强?【附真实图片对比】
前端
谦谦橘子5 分钟前
服务端渲染原理解析姐妹篇
前端·javascript·react.js
i编程_撸码6 分钟前
webpack详细打包配置,包含性能优化、资源处理...
前端
小小小小宇7 分钟前
React 中 useMemo 和 useCallback 源码原理
前端
Trae首席推荐官10 分钟前
Trae 版本更新|支持自定义智能体、MCP等,打造个人专属“AI 工程师”
前端·trae
木三_copy11 分钟前
前端截图工具--html2canvas和html-to-image的一些踩坑
前端
小桥风满袖13 分钟前
Three.js-硬要自学系列7 (查看几何体顶点位置和索引、旋转,缩放,平移几何体)
前端·css·three.js
kim__jin15 分钟前
Vue3 使用项目内嵌iFrame
前端
独立开阀者_FwtCoder28 分钟前
# 一天 Star 破万的开源项目「GitHub 热点速览」
前端·javascript·面试