React 19 源码揭秘(七):Commit 阶段与 DOM 操作
本文深入 Commit 阶段源码,看看 React 是如何将 Fiber 树的变化同步到 DOM 的。
前言
Render 阶段计算出了"要做什么",Commit 阶段则是"真正去做"。
这个阶段会:
- 执行 DOM 操作(增删改)
- 调用生命周期方法
- 执行 useLayoutEffect 和 useEffect
一、Commit 阶段概览
Commit 阶段分为三个子阶段:
┌─────────────────────────────────────────────────────────┐
│ Commit 阶段 │
├─────────────────────────────────────────────────────────┤
│ │
│ Before Mutation ──► Mutation ──► Layout │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ getSnapshot DOM 操作 useLayoutEffect │
│ BeforeUpdate componentDidMount │
│ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────┐
│ Passive Effects │
│ (异步) │
│ │
│ useEffect │
└─────────────────────────┘
二、commitRoot 入口
function commitRoot(root, recoverableErrors, transitions, ...) {
const finishedWork = root.finishedWork;
// 1. 调度 useEffect(异步)
if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags) {
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
// 2. 检查是否有副作用需要处理
const subtreeHasEffects = (finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
if (subtreeHasEffects || rootHasEffect) {
// 3. Before Mutation 阶段
commitBeforeMutationEffects(root, finishedWork);
// 4. Mutation 阶段
commitMutationEffects(root, finishedWork, lanes);
// 5. 切换 Fiber 树!
root.current = finishedWork;
// 6. Layout 阶段
commitLayoutEffects(finishedWork, root, lanes);
} else {
root.current = finishedWork;
}
// 7. 确保后续更新被调度
ensureRootIsScheduled(root);
}
三、Before Mutation 阶段
DOM 变更前,读取 DOM 状态。
function commitBeforeMutationEffects(root, firstChild) {
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
}
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
// 处理 Snapshot 标记
if ((fiber.flags & Snapshot) !== NoFlags) {
switch (fiber.tag) {
case ClassComponent: {
const instance = fiber.stateNode;
// 调用 getSnapshotBeforeUpdate
const snapshot = instance.getSnapshotBeforeUpdate(
fiber.elementType === fiber.type
? prevProps
: resolveDefaultProps(fiber.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
break;
}
case HostRoot: {
// 清空容器
clearContainer(root.containerInfo);
break;
}
}
}
nextEffect = fiber.return;
}
}
主要工作
- 调用类组件的
getSnapshotBeforeUpdate - 清空根容器(首次渲染时)
四、Mutation 阶段
执行 DOM 操作,这是最核心的阶段。
function commitMutationEffects(root, finishedWork, lanes) {
commitMutationEffectsOnFiber(finishedWork, root, lanes);
}
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
const flags = finishedWork.flags;
// 1. 处理 Ref 卸载
if (flags & Ref) {
const current = finishedWork.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 2. 根据 flags 执行不同操作
const primaryFlags = flags & (Placement | Update | ChildDeletion);
switch (primaryFlags) {
case Placement: {
// 插入 DOM
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
break;
}
case Update: {
// 更新 DOM
commitWork(finishedWork);
break;
}
case Placement | Update: {
// 先插入,再更新
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
commitWork(finishedWork);
break;
}
case ChildDeletion: {
// 删除子节点
commitDeletions(finishedWork.deletions, finishedWork);
break;
}
}
}
commitPlacement(插入 DOM)
function commitPlacement(finishedWork) {
// 1. 找到最近的 Host 父节点
const parentFiber = getHostParentFiber(finishedWork);
const parentDOM = parentFiber.stateNode;
// 2. 找到插入位置(兄弟节点)
const before = getHostSibling(finishedWork);
// 3. 插入 DOM
if (before) {
insertBefore(parentDOM, finishedWork.stateNode, before);
} else {
appendChild(parentDOM, finishedWork.stateNode);
}
}
commitWork(更新 DOM)
function commitWork(finishedWork) {
switch (finishedWork.tag) {
case HostComponent: {
const instance = finishedWork.stateNode;
if (instance !== null) {
const newProps = finishedWork.memoizedProps;
const oldProps = finishedWork.alternate?.memoizedProps;
const type = finishedWork.type;
// 更新 DOM 属性
commitUpdate(instance, type, oldProps, newProps, finishedWork);
}
break;
}
case HostText: {
const textInstance = finishedWork.stateNode;
const newText = finishedWork.memoizedProps;
// 更新文本内容
commitTextUpdate(textInstance, newText);
break;
}
case FunctionComponent: {
// 执行 useInsertionEffect 和 useLayoutEffect 的销毁函数
commitHookEffectListUnmount(HookInsertion | HookHasEffect, finishedWork);
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
break;
}
}
}
commitDeletions(删除)
function commitDeletions(deletions, parentFiber) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
// 递归卸载
commitUnmount(childToDelete);
// 从 DOM 中移除
removeChild(parentDOM, childToDelete.stateNode);
}
}
function commitUnmount(fiber) {
switch (fiber.tag) {
case FunctionComponent: {
// 执行 useEffect 和 useLayoutEffect 的销毁函数
commitHookEffectListUnmount(HookPassive, fiber);
commitHookEffectListUnmount(HookLayout, fiber);
break;
}
case ClassComponent: {
// 调用 componentWillUnmount
const instance = fiber.stateNode;
instance.componentWillUnmount();
break;
}
}
// 递归处理子节点
let child = fiber.child;
while (child !== null) {
commitUnmount(child);
child = child.sibling;
}
}
五、切换 Fiber 树
在 Mutation 和 Layout 之间,有一行关键代码:
root.current = finishedWork;
这行代码将 workInProgress 树变成 current 树,完成双缓冲切换。
为什么在这个时机?
- Mutation 阶段:需要访问旧的 DOM 状态(componentWillUnmount)
- Layout 阶段:需要访问新的 DOM 状态(componentDidMount)
所以切换发生在两者之间。
六、Layout 阶段
DOM 变更后,可以安全读取 DOM。
function commitLayoutEffects(finishedWork, root, lanes) {
commitLayoutEffectOnFiber(root, finishedWork.alternate, finishedWork, lanes);
}
function commitLayoutEffectOnFiber(root, current, finishedWork, lanes) {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent: {
// 执行 useLayoutEffect 的创建函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
break;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (current === null) {
// 首次渲染
instance.componentDidMount();
} else {
// 更新
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
}
// 处理 setState 回调
commitUpdateQueue(finishedWork, finishedWork.updateQueue, instance);
break;
}
case HostRoot: {
// 处理 ReactDOM.render 回调
commitUpdateQueue(finishedWork, finishedWork.updateQueue, null);
break;
}
}
// 绑定 Ref
if (flags & Ref) {
commitAttachRef(finishedWork);
}
}
commitAttachRef
function commitAttachRef(finishedWork) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
if (typeof ref === 'function') {
ref(instance);
} else {
ref.current = instance;
}
}
}
七、Passive Effects(useEffect)
useEffect 是异步执行的,在 Commit 阶段只是调度:
// commitRoot 中
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
flushPassiveEffects
function flushPassiveEffects() {
if (rootWithPendingPassiveEffects !== null) {
// 1. 执行所有销毁函数
commitPassiveUnmountEffects(root.current);
// 2. 执行所有创建函数
commitPassiveMountEffects(root, root.current);
}
}
执行顺序
组件树:
App
└── Parent
└── Child
Mount 时:
1. Child useLayoutEffect 创建
2. Parent useLayoutEffect 创建
3. App useLayoutEffect 创建
4. ─── 浏览器绘制 ───
5. Child useEffect 创建
6. Parent useEffect 创建
7. App useEffect 创建
八、Flags 标记
// ReactFiberFlags.js
export const NoFlags = 0b0000000000000000000000000000;
export const Placement = 0b0000000000000000000000000010; // 插入
export const Update = 0b0000000000000000000000000100; // 更新
export const ChildDeletion = 0b0000000000000000000000010000; // 删除子节点
export const Snapshot = 0b0000000100000000000000000000; // getSnapshotBeforeUpdate
export const Passive = 0b0000100000000000000000000000; // useEffect
export const Ref = 0b0001000000000000000000000000; // ref
// 阶段掩码
export const BeforeMutationMask = Snapshot;
export const MutationMask = Placement | Update | ChildDeletion | Ref;
export const LayoutMask = Update | Callback | Ref;
export const PassiveMask = Passive | ChildDeletion;
九、调试技巧
// 在这些位置打断点:
// 入口
commitRoot // ReactFiberWorkLoop.js
// Before Mutation
commitBeforeMutationEffects // ReactFiberCommitWork.js
// Mutation
commitMutationEffects // ReactFiberCommitWork.js
commitPlacement // DOM 插入
commitWork // DOM 更新
commitDeletions // DOM 删除
// Layout
commitLayoutEffects // ReactFiberCommitWork.js
// Passive
flushPassiveEffects // ReactFiberWorkLoop.js
小结
Commit 阶段的核心流程:
- Before Mutation:读取 DOM 状态,getSnapshotBeforeUpdate
- Mutation:执行 DOM 操作(增删改)
- 切换 Fiber 树:root.current = finishedWork
- Layout:useLayoutEffect、componentDidMount/Update、绑定 ref
- Passive:异步执行 useEffect
关键点:
- Commit 阶段不可中断
- useLayoutEffect 同步执行,useEffect 异步执行
- Fiber 树切换发生在 Mutation 和 Layout 之间
📦 配套源码:github.com/220529/reac…
如果觉得有帮助,欢迎点赞收藏 👍