Skip to content

Layout Effects 阶段详解

在 DOM Mutations 阶段,React 已经将所有计算出的变更应用到了实际的 DOM 上。Layout Effects 阶段在浏览器进行下一次绘制(paint)之前同步执行。这意味着在此阶段执行的代码会阻塞浏览器的渲染。

Layout Effects 内部流程

  1. 开始遍历 Effect List (Layout Effects):

    • React 遍历那些在 Render Phase 被标记为需要 Layout Effects 的 Fiber 节点 (例如,带有 LayoutMask flag)。
  2. 对于每个相关的 Fiber 节点:

    • 函数组件 (FunctionComponent):
      1. 执行清理: 检查该 Fiber 上的 useLayoutEffect Hooks。如果某个 useLayoutEffect 在前一次渲染中执行过,并且现在该组件正在卸载,或者该 Hook 的依赖项发生了变化,则同步执行其上一次返回的清理函数
      2. 执行 Effect: 如果组件是新挂载的,或者某个 useLayoutEffect 的依赖项发生了变化(或者没有依赖项),则同步执行该 Hook 的回调函数
      3. 存储新清理函数: 将该回调函数返回的新清理函数存储起来,以便在未来需要时(组件卸载或下一次依赖项变化时)执行。
    • 类组件 (ClassComponent):
      1. 获取组件实例。
      2. componentDidMount: 如果该组件是首次挂载(current Fiber 为 null),则调用其实例的 componentDidMount() 方法。
      3. componentDidUpdate: 如果该组件是更新(current Fiber 不为 null),则调用其实例的 componentDidUpdate(prevProps, prevState, snapshot) 方法。
        • prevPropsprevState 来自于 current Fiber。
        • snapshot 是在 "Before Mutation Effects" 阶段从 getSnapshotBeforeUpdate() 获取的值。调用后,内部存储的 snapshot 会被清除。
    • Ref 处理: 此时,所有 DOM 节点的 refs 都应该已经被附加(在 DOM Mutation 阶段或之前)。类组件实例的 refs 也已可用。因此,在 componentDidMount/UpdateuseLayoutEffect 中可以安全地访问 refs。
  3. 错误处理:

    • 在执行上述所有函数(生命周期方法、Hook 回调和清理函数)时,React 都会使用 try-catch。如果发生错误,它会沿着 Fiber 树向上查找最近的错误边界来处理该错误。
  4. 同步完成:

    • 所有 Layout Effects 都执行完毕后,这个阶段才算完成。由于是同步的,这会阻塞主线程,直到所有操作结束。
  5. 浏览器绘制:

    • 一旦 Layout Effects 阶段完成,JavaScript 执行栈会释放,浏览器现在可以自由地进行计算布局、绘制(paint)并将更新显示到屏幕上。由于 Layout Effects 在绘制前执行,它们所做的任何 DOM 读取或修改都会被包含在即将到来的绘制中。
  6. 调度 Passive Effects:

    • 在 Layout Effects 完成后,React 会调度 Passive Effects (即 useEffect Hooks) 异步执行。

由于这些操作通常依赖于刚刚更新的 DOM 结构,并且它们的结果需要在用户看到下一次屏幕更新之前生效,以避免出现视觉上的不一致或闪烁。例如,如果需要在组件挂载后测量其宽度并据此设置另一个元素的样式,这个过程必须同步完成。所以这个阶段确保了那些需要与 DOM 布局同步的逻辑能够在正确的时间点执行,从而保证了 UI 的一致性和正确性。

流程图

核心函数

commitLayoutEffects 函数

commitLayoutEffects 函数是 Layout Effects 阶段的入口点。它的主要职责是初始化一些全局变量(如 inProgressLanesinProgressRoot),重置组件effect的计时器,然后调用 commitLayoutEffectOnFiber 来实际执行 Layout Effects。

javascript
// ... existing code ...
export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;

  resetComponentEffectTimers();

  const current = finishedWork.alternate;
  commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}
// ... existing code ...

主要工作流程:

  1. 设置全局状态
    • inProgressLanes:设置为当前提交的 lanes
    • inProgressRoot:设置为当前的 root。 这些全局变量用于在 effect 执行期间提供上下文信息。
  2. 重置 Effect 计时器
    • 调用 resetComponentEffectTimers() 来重置用于性能分析的组件 effect 相关的计时器。
  3. 获取 current Fiber
    • const current = finishedWork.alternate; 获取 finishedWork 对应的 current Fiber (即更新前的 Fiber 树中的对应节点)。这对于比较新旧状态、执行更新相关的生命周期方法等非常重要。
  4. 调用 commitLayoutEffectOnFiber
    • 这是核心步骤,将实际的 Layout Effects 处理委托给 commitLayoutEffectOnFiber 函数。
  5. 清理全局状态
    • commitLayoutEffectOnFiber 执行完毕后,将 inProgressLanesinProgressRoot 设置回 null,清理全局状态。

commitLayoutEffectOnFiber 函数

commitLayoutEffectOnFiber 函数是递归遍历 Fiber 树并执行 Layout Effects 的核心函数。它会根据 Fiber 节点的 tag (类型) 和 flags (标记) 来执行不同的操作,例如调用 componentDidMountcomponentDidUpdate生命周期方法,执行 useLayoutEffect Hook 的回调,以及处理 ref 的附加等。

commitLayoutEffectOnFiber 函数源码

javascript
// ... existing code ...
function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  const prevEffectStart = pushComponentEffectStart();
  const prevEffectDuration = pushComponentEffectDuration();
  const prevEffectErrors = pushComponentEffectErrors();
  // When updating this function, also update reappearLayoutEffects, which does
  // most of the same things when an offscreen tree goes from hidden -> visible.
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      recursivelyTraverseLayoutEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
      );
      if (flags & Update) {
        commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
      }
      break;
    }
    case ClassComponent: {
      recursivelyTraverseLayoutEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
      );
      if (flags & Update) {
        commitClassLayoutLifecycles(finishedWork, current);
      }

      if (flags & Callback) {
        commitClassCallbacks(finishedWork);
      }

      if (flags & Ref) {
        safelyAttachRef(finishedWork, finishedWork.return);
      }
      break;
    }
    case HostRoot: {
      const prevProfilerEffectDuration = pushNestedEffectDurations();
      recursivelyTraverseLayoutEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
      );
      if (flags & Callback) {
        commitRootCallbacks(finishedWork);
      }
      if (enableProfilerTimer && enableProfilerCommitHooks) {
        finishedRoot.effectDuration += popNestedEffectDurations(
          prevProfilerEffectDuration,
        );
      }
      break;
    }
    case HostSingleton: {
      if (supportsSingletons) {
        // ... HostSingleton specific logic ...
        if (current === null && flags & Update) {
          commitHostSingletonAcquisition(finishedWork);
        }
      }
      // Fallthrough
    }
    case HostHoistable:
    case HostComponent: {
      recursivelyTraverseLayoutEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
      );

      if (current === null) { // 首次挂载
        if (flags & Update) {
          commitHostMount(finishedWork); // 例如:处理 autoFocus
        } else if (flags & Hydrate) {
          commitHostHydratedInstance(finishedWork);
        }
      }

      if (flags & Ref) {
        safelyAttachRef(finishedWork, finishedWork.return);
      }
      break;
    }
    case Profiler: {
      // ... Profiler specific logic ...
      break;
    }
    case ActivityComponent: {
      // ... ActivityComponent specific logic ...
      break;
    }
    case SuspenseComponent: {
      // ... SuspenseComponent specific logic ...
      break;
    }
    // ... other cases like OffscreenComponent, ScopeComponent, ViewTransitionComponent etc. ...
  }

  popComponentEffectStart(prevEffectStart);
  popComponentEffectDuration(prevEffectDuration);
  popComponentEffectErrors(prevEffectErrors);
}
// ... existing code ...

主要工作流程和逻辑:

  1. 性能分析准备
    • pushComponentEffectStart()pushComponentEffectDuration()pushComponentEffectErrors():这些函数用于在执行 effect 前记录一些状态,以便后续进行性能分析和错误追踪。
  2. 递归遍历子节点
    • 对于大多数组件类型,首先会调用 recursivelyTraverseLayoutEffects 来递归处理子节点的 Layout Effects。这确保了子组件的 Layout Effects 会在父组件之前执行。
  3. 根据 Fiber 类型 (tag) 处理
    • FunctionComponent, ForwardRef, SimpleMemoComponent
      • 如果存在 Update flag,则调用 commitHookLayoutEffects 来执行 useLayoutEffect Hook 的回调函数。HookLayout | HookHasEffect 标记指示执行那些带有 Layout 标签且实际存在 effect 的 Hook。
    • ClassComponent
      • 如果存在 Update flag,则调用 commitClassLayoutLifecycles。这个函数内部会根据 current 是否为 null (即是挂载还是更新) 来调用 componentDidMountcomponentDidUpdate
      • 如果存在 Callback flag,则调用 commitClassCallbacks 来执行 setState 的回调函数。
      • 如果存在 Ref flag,则调用 safelyAttachRef 来附加 ref。
    • HostRoot (根节点):
      • 如果存在 Callback flag (通常在 hydrateRootrender 的回调中使用),则调用 commitRootCallbacks
      • 处理 Profiler 相关的 effect 时长统计。
    • HostComponent (DOM 元素):
      • 如果是首次挂载 (current === null) 并且有 Update flag,则调用 commitHostMount。这个函数可能会执行一些只有在 DOM 元素首次挂载后才需要执行的操作,例如表单元素的 autoFocus
      • 如果是 Hydrate 场景,则调用 commitHostHydratedInstance
      • 如果存在 Ref flag,则调用 safelyAttachRef 来附加 ref。
    • HostSingleton:处理单例组件的获取逻辑。
    • Profiler:处理 Profiler 组件的更新和 effect 时长统计。
    • ActivityComponent, SuspenseComponent, OffscreenComponent, ViewTransitionComponent:这些组件类型也有各自特定的 Layout Effects 处理逻辑,例如处理 hydration 回调、附加/分离 ref、处理视图过渡等。
  4. 性能分析收尾
    • popComponentEffectStart()popComponentEffectDuration()popComponentEffectErrors():恢复之前记录的性能分析状态。

这两个函数共同构成了 React Commit 阶段中 Layout Effects 子阶段的核心逻辑,负责在 DOM 变更后,同步执行需要读取/操作 DOM 布局的副作用。


微信公众号二维码