Appearance
Layout Effects 阶段详解
在 DOM Mutations 阶段,React 已经将所有计算出的变更应用到了实际的 DOM 上。Layout Effects 阶段在浏览器进行下一次绘制(paint)之前同步执行。这意味着在此阶段执行的代码会阻塞浏览器的渲染。
Layout Effects 内部流程
开始遍历 Effect List (Layout Effects):
- React 遍历那些在 Render Phase 被标记为需要 Layout Effects 的 Fiber 节点 (例如,带有
LayoutMask
flag)。
- React 遍历那些在 Render Phase 被标记为需要 Layout Effects 的 Fiber 节点 (例如,带有
对于每个相关的 Fiber 节点:
- 函数组件 (
FunctionComponent
):- 执行清理: 检查该 Fiber 上的
useLayoutEffect
Hooks。如果某个useLayoutEffect
在前一次渲染中执行过,并且现在该组件正在卸载,或者该 Hook 的依赖项发生了变化,则同步执行其上一次返回的清理函数。 - 执行 Effect: 如果组件是新挂载的,或者某个
useLayoutEffect
的依赖项发生了变化(或者没有依赖项),则同步执行该 Hook 的回调函数。 - 存储新清理函数: 将该回调函数返回的新清理函数存储起来,以便在未来需要时(组件卸载或下一次依赖项变化时)执行。
- 执行清理: 检查该 Fiber 上的
- 类组件 (
ClassComponent
):- 获取组件实例。
componentDidMount
: 如果该组件是首次挂载(current
Fiber 为null
),则调用其实例的componentDidMount()
方法。componentDidUpdate
: 如果该组件是更新(current
Fiber 不为null
),则调用其实例的componentDidUpdate(prevProps, prevState, snapshot)
方法。prevProps
和prevState
来自于current
Fiber。snapshot
是在 "Before Mutation Effects" 阶段从getSnapshotBeforeUpdate()
获取的值。调用后,内部存储的snapshot
会被清除。
- Ref 处理: 此时,所有 DOM 节点的 refs 都应该已经被附加(在 DOM Mutation 阶段或之前)。类组件实例的 refs 也已可用。因此,在
componentDidMount
/Update
和useLayoutEffect
中可以安全地访问 refs。
- 函数组件 (
错误处理:
- 在执行上述所有函数(生命周期方法、Hook 回调和清理函数)时,React 都会使用
try-catch
。如果发生错误,它会沿着 Fiber 树向上查找最近的错误边界来处理该错误。
- 在执行上述所有函数(生命周期方法、Hook 回调和清理函数)时,React 都会使用
同步完成:
- 所有 Layout Effects 都执行完毕后,这个阶段才算完成。由于是同步的,这会阻塞主线程,直到所有操作结束。
浏览器绘制:
- 一旦 Layout Effects 阶段完成,JavaScript 执行栈会释放,浏览器现在可以自由地进行计算布局、绘制(paint)并将更新显示到屏幕上。由于 Layout Effects 在绘制前执行,它们所做的任何 DOM 读取或修改都会被包含在即将到来的绘制中。
调度 Passive Effects:
- 在 Layout Effects 完成后,React 会调度 Passive Effects (即
useEffect
Hooks) 异步执行。
- 在 Layout Effects 完成后,React 会调度 Passive Effects (即
由于这些操作通常依赖于刚刚更新的 DOM 结构,并且它们的结果需要在用户看到下一次屏幕更新之前生效,以避免出现视觉上的不一致或闪烁。例如,如果需要在组件挂载后测量其宽度并据此设置另一个元素的样式,这个过程必须同步完成。所以这个阶段确保了那些需要与 DOM 布局同步的逻辑能够在正确的时间点执行,从而保证了 UI 的一致性和正确性。
流程图
核心函数
commitLayoutEffects
函数
commitLayoutEffects
函数是 Layout Effects 阶段的入口点。它的主要职责是初始化一些全局变量(如 inProgressLanes
和 inProgressRoot
),重置组件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 ...
主要工作流程:
- 设置全局状态:
inProgressLanes
:设置为当前提交的lanes
。inProgressRoot
:设置为当前的root
。 这些全局变量用于在 effect 执行期间提供上下文信息。
- 重置 Effect 计时器:
- 调用
resetComponentEffectTimers()
来重置用于性能分析的组件 effect 相关的计时器。
- 调用
- 获取
current
Fiber:const current = finishedWork.alternate;
获取finishedWork
对应的current
Fiber (即更新前的 Fiber 树中的对应节点)。这对于比较新旧状态、执行更新相关的生命周期方法等非常重要。
- 调用
commitLayoutEffectOnFiber
:- 这是核心步骤,将实际的 Layout Effects 处理委托给
commitLayoutEffectOnFiber
函数。
- 这是核心步骤,将实际的 Layout Effects 处理委托给
- 清理全局状态:
- 在
commitLayoutEffectOnFiber
执行完毕后,将inProgressLanes
和inProgressRoot
设置回null
,清理全局状态。
- 在
commitLayoutEffectOnFiber
函数
commitLayoutEffectOnFiber
函数是递归遍历 Fiber 树并执行 Layout Effects 的核心函数。它会根据 Fiber 节点的 tag
(类型) 和 flags
(标记) 来执行不同的操作,例如调用 componentDidMount
、componentDidUpdate
生命周期方法,执行 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 ...
主要工作流程和逻辑:
- 性能分析准备:
pushComponentEffectStart()
,pushComponentEffectDuration()
,pushComponentEffectErrors()
:这些函数用于在执行 effect 前记录一些状态,以便后续进行性能分析和错误追踪。
- 递归遍历子节点:
- 对于大多数组件类型,首先会调用
recursivelyTraverseLayoutEffects
来递归处理子节点的 Layout Effects。这确保了子组件的 Layout Effects 会在父组件之前执行。
- 对于大多数组件类型,首先会调用
- 根据 Fiber 类型 (tag) 处理:
FunctionComponent
,ForwardRef
,SimpleMemoComponent
:- 如果存在
Update
flag,则调用commitHookLayoutEffects
来执行useLayoutEffect
Hook 的回调函数。HookLayout | HookHasEffect
标记指示执行那些带有Layout
标签且实际存在 effect 的 Hook。
- 如果存在
ClassComponent
:- 如果存在
Update
flag,则调用commitClassLayoutLifecycles
。这个函数内部会根据current
是否为null
(即是挂载还是更新) 来调用componentDidMount
或componentDidUpdate
。 - 如果存在
Callback
flag,则调用commitClassCallbacks
来执行setState
的回调函数。 - 如果存在
Ref
flag,则调用safelyAttachRef
来附加 ref。
- 如果存在
HostRoot
(根节点):- 如果存在
Callback
flag (通常在hydrateRoot
或render
的回调中使用),则调用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、处理视图过渡等。
- 性能分析收尾:
popComponentEffectStart()
,popComponentEffectDuration()
,popComponentEffectErrors()
:恢复之前记录的性能分析状态。
这两个函数共同构成了 React Commit 阶段中 Layout Effects 子阶段的核心逻辑,负责在 DOM 变更后,同步执行需要读取/操作 DOM 布局的副作用。
