在 Render 阶段,我们得到的并不是 DOM,而是一条 Effect List 。
那么,这些
Placement
、Update
、Deletion
副作用标记,究竟是怎么落实到真实的 DOM 上的呢?
答案就是:Commit 阶段。
切记、切记、切记:
- Render 阶段:纯计算,可能被中断。
- Commit 阶段:副作用落地,不可中断。
一、Effect List 概述
1.1 什么是 Effect List?
javascript
/* 💡 Effect List 的定义:
* - 一个单向链表,包含所有需要执行副作用的 Fiber 节点
* - 在 Render 阶段构建
* - 在 Commit 阶段消费
*
* 为什么需要 Effect List?
* - Fiber 树可能有成千上万个节点
* - 但只有少数节点需要执行副作用(DOM 操作、生命周期等)
* - Effect List 只包含这些节点,避免遍历整棵树
*/
// 旧版 React(<= 17)的 Effect List 结构:
FiberNode {
// 指向 Effect List 的指针
firstEffect: Fiber | null, // 子树 Effect List 的第一个节点
lastEffect: Fiber | null, // 子树 Effect List 的最后一个节点
nextEffect: Fiber | null, // Effect List 的下一个节点
// 副作用标记
flags: number, // 自身的副作用
// 其他属性...
}
// 新版 React(>= 18)的变化:
FiberNode {
// 不再使用 firstEffect、lastEffect、nextEffect
// 改用 flags 和 subtreeFlags
flags: number, // 自身的副作用
subtreeFlags: number, // 子树的副作用(所有子节点 flags 的集合)
// Commit 阶段直接遍历 Fiber 树,根据 flags 判断是否需要处理
}
1.2 Effect List 在整个流程中的位置
sql
React 渲染流程:
触发更新
↓
Render 阶段开始
↓
beginWork(递阶段)
├─ 创建/更新 Fiber 节点
├─ 标记 flags(Placement、Update 等)
└─ 返回子节点
↓
completeWork(归阶段)
├─ 创建/更新 DOM
├─ 🔥 收集副作用(bubbleProperties)
│ ├─ 收集子节点的 flags
│ ├─ 合并到 subtreeFlags
│ └─ 构建 Effect List(旧版)
└─ 返回父节点
↓
Render 阶段完成
↓
rootFiber.firstEffect 指向 Effect List 头(旧版)
rootFiber.subtreeFlags 包含所有副作用(新版)
↓
Commit 阶段开始
↓
遍历 Effect List / Fiber 树
├─ 执行 DOM 操作
├─ 调用生命周期
└─ 执行 useEffect
↓
Commit 阶段完成
↓
屏幕更新
二、副作用的类型(Flags)
2.1 常见的 Flags 定义
javascript
// 📍 位置:ReactFiberFlags.js
export const NoFlags = 0b0000000000000000000000000000;
// 🔥 Fiber 节点自身的副作用
export const PerformedWork = 0b0000000000000000000000000001;
export const Placement = 0b0000000000000000000000000010; // 插入
export const Update = 0b0000000000000000000000000100; // 更新
export const Deletion = 0b0000000000000000000000001000; // 删除
export const ContentReset = 0b0000000000000000000000010000;
export const Callback = 0b0000000000000000000000100000;
export const DidCapture = 0b0000000000000000000001000000;
export const Ref = 0b0000000000000000000010000000; // Ref 需要更新
export const Snapshot = 0b0000000000000000000100000000; // getSnapshotBeforeUpdate
export const Passive = 0b0000000000000000001000000000; // useEffect
export const Hydrating = 0b0000000000000000010000000000;
export const Visibility = 0b0000000000000000100000000000;
export const StoreConsistency = 0b0000000000000001000000000000;
// 🔥 生命周期相关
export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot;
// 🔥 Host 副作用(DOM 操作)
export const HostEffectMask = 0b0000000000000000011111111111;
// 🔥 所有副作用
export const BeforeMutationMask = Update | Snapshot;
export const MutationMask =
Placement | Update | Deletion | ContentReset | Ref | Hydrating | Visibility;
export const LayoutMask = Update | Callback | Ref | Visibility;
2.2 Flags 的使用方式
javascript
// 添加 flag
fiber.flags |= Placement;
fiber.flags |= Update;
// 检查 flag
if (fiber.flags & Placement) {
// 节点需要插入
}
if (fiber.flags & Update) {
// 节点需要更新
}
// 移除 flag
fiber.flags &= ~Placement;
// 检查是否有任何副作用
if (fiber.flags !== NoFlags) {
// 节点有副作用
}
// 示例:一个节点可能同时有多个 flags
fiber.flags = Placement | Update | Ref;
// 二进制:0b0000000000000000000010000110
2.3 不同阶段的 Flags
javascript
// ========== Render 阶段标记 ==========
// beginWork 中标记:
case ClassComponent: {
if (instance === null) {
// 首次挂载,标记 Placement
workInProgress.flags |= Placement;
}
if (typeof instance.componentDidMount === 'function') {
// 标记 Update(生命周期)
workInProgress.flags |= Update;
}
}
// reconcileChildren 中标记:
if (shouldTrackSideEffects) {
if (current === null) {
// 新增节点
newFiber.flags |= Placement;
} else {
// 更新节点
if (oldProps !== newProps) {
newFiber.flags |= Update;
}
}
}
// completeWork 中标记:
if (workInProgress.ref !== null) {
// Ref 需要更新
workInProgress.flags |= Ref;
}
// ========== Commit 阶段消费 ==========
// Before Mutation 阶段:
if (fiber.flags & Snapshot) {
// 调用 getSnapshotBeforeUpdate
}
// Mutation 阶段:
if (fiber.flags & Placement) {
// 插入 DOM
commitPlacement(fiber);
}
if (fiber.flags & Update) {
// 更新 DOM 属性
commitWork(fiber);
}
if (fiber.flags & Deletion) {
// 删除 DOM
commitDeletion(fiber);
}
// Layout 阶段:
if (fiber.flags & Update) {
// 调用 componentDidMount/componentDidUpdate
commitLifeCycles(fiber);
}
if (fiber.flags & Callback) {
// 调用 setState 的 callback
}
三、Effect List 的构建过程
3.1 completeWork 中的副作用收集
javascript
// 📍 位置:ReactFiberCompleteWork.new.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// ... 创建 DOM、更新属性等
// 🔥 关键:在返回前收集副作用
bubbleProperties(workInProgress);
return null;
}
3.2 旧版 Effect List 的构建(React <= 17)
javascript
// 📍 位置:ReactFiberWorkLoop.old.js(旧版)
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 调用 completeWork
let next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
workInProgress = next;
return;
}
// ========== 🔥 构建 Effect List 🔥 ==========
if (
returnFiber !== null &&
(completedWork.flags & Incomplete) === NoFlags
) {
// 步骤1:将子节点的 Effect List 附加到父节点
if (returnFiber.firstEffect === null) {
// 父节点还没有 Effect List,直接使用子节点的
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
// 子节点有 Effect List
if (returnFiber.lastEffect !== null) {
// 父节点已经有 Effect List,连接起来
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
// 更新父节点的 lastEffect
returnFiber.lastEffect = completedWork.lastEffect;
}
// 步骤2:将当前节点加入 Effect List(如果有副作用)
const flags = completedWork.flags;
// 跳过 PerformedWork 和 Incomplete
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
// 连接到父节点的 Effect List 末尾
returnFiber.lastEffect.nextEffect = completedWork;
} else {
// 父节点的 Effect List 是空的
returnFiber.firstEffect = completedWork;
}
// 更新 lastEffect
returnFiber.lastEffect = completedWork;
}
}
// 继续处理兄弟节点或回溯
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
Effect List 构建的可视化:
markdown
Fiber 树结构:
App
/ \
div Content
/
header
假设各节点的 flags:
- App: Update (componentDidMount)
- div: NoFlags
- header: Placement (新增)
- Content: Update (componentDidUpdate)
构建过程(从下往上):
1. header completeWork:
- header.flags = Placement
- header.firstEffect = null
- header.lastEffect = null
2. header 添加到 div 的 Effect List:
- div.firstEffect = header
- div.lastEffect = header
- 因为 header.flags > PerformedWork
3. Content completeWork:
- Content.flags = Update
- Content.firstEffect = null
- Content.lastEffect = null
4. Content 添加到 App 的 Effect List:
- App.firstEffect = Content
- App.lastEffect = Content
5. div 回溯到 App:
- div 的子树 Effect List: header
- div 自身无副作用 (flags = NoFlags)
- 合并:
- App.lastEffect.nextEffect = div.firstEffect (Content.nextEffect = header)
- App.lastEffect = div.lastEffect (App.lastEffect = header)
6. App 添加自己:
- App.flags = Update
- App.lastEffect.nextEffect = App (header.nextEffect = App)
- App.lastEffect = App
最终 Effect List:
App.firstEffect → Content → header → App → null
↓ ↓ ↓
nextEffect nextEffect nextEffect
Commit 阶段按顺序执行:
1. Content (Update)
2. header (Placement)
3. App (Update)
四、bubbleProperties - 副作用冒泡
4.1 新版实现(React >= 18)
javascript
// 📍 位置:ReactFiberCompleteWork.new.js
function bubbleProperties(completedWork: Fiber) {
/* 💡 新版的副作用收集策略:
* - 不再构建 Effect List 链表
* - 使用 subtreeFlags 收集子树的所有副作用
* - Commit 阶段遍历 Fiber 树,根据 flags 和 subtreeFlags 判断
*/
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;
let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;
if (!didBailout) {
// === 首次渲染或有更新 ===
let child = completedWork.child;
while (child !== null) {
// 🔥 收集子节点的 lanes
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
// 🔥 收集子节点的 flags
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
// 设置 return 指针
child.return = completedWork;
child = child.sibling;
}
// 🔥 将收集的副作用设置到当前节点
completedWork.subtreeFlags |= subtreeFlags;
} else {
// === Bailout 优化路径 ===
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes)
);
// Bailout 时只收集静态副作用
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;
child.return = completedWork;
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags;
}
completedWork.childLanes = newChildLanes;
return didBailout;
}
4.2 subtreeFlags 的使用
javascript
// Render 阶段结束后:
rootFiber {
flags: NoFlags,
subtreeFlags: Placement | Update | Ref, // 子树有这些副作用
child: AppFiber,
}
↓
AppFiber {
flags: Update, // componentDidMount
subtreeFlags: Placement | Ref, // 子树有这些副作用
child: divFiber,
}
↓
divFiber {
flags: NoFlags,
subtreeFlags: Placement, // 子树有插入操作
child: headerFiber,
}
↓
headerFiber {
flags: Placement, // 新增节点
subtreeFlags: NoFlags, // 叶子节点
}
// Commit 阶段使用:
function commitMutationEffects(root, finishedWork) {
// 递归遍历 Fiber 树
commitMutationEffectsOnFiber(finishedWork, root);
}
function commitMutationEffectsOnFiber(finishedWork, root) {
const flags = finishedWork.flags;
// 🔥 先处理子树
if (finishedWork.subtreeFlags & MutationMask) {
// 子树有需要处理的副作用,递归处理
let child = finishedWork.child;
while (child !== null) {
commitMutationEffectsOnFiber(child, root);
child = child.sibling;
}
}
// 🔥 再处理自己
if (flags & MutationMask) {
// 自己有副作用,执行对应操作
switch (finishedWork.tag) {
case HostComponent: {
if (flags & Placement) {
commitPlacement(finishedWork);
}
if (flags & Update) {
commitWork(current, finishedWork);
}
// ...
}
}
}
}
五、完整的 Effect List 构建示例
5.1 示例代码
jsx
class App extends React.Component {
state = { show: true };
componentDidMount() {
console.log("App mounted");
}
render() {
return (
<div>
<header ref={this.headerRef}>Header</header>
{this.state.show && <Content />}
</div>
);
}
}
class Content extends React.Component {
componentDidUpdate() {
console.log("Content updated");
}
render() {
return (
<div>
<p>Text 1</p>
<p>Text 2</p>
</div>
);
}
}
// 首次渲染
ReactDOM.render(<App />, root);
5.2 首次渲染的 Effect 收集
ini
=== Render 阶段(归阶段)===
1. p("Text 1") completeWork:
flags: Placement
subtreeFlags: NoFlags
firstEffect: null
lastEffect: null
2. p("Text 2") completeWork:
flags: Placement
subtreeFlags: NoFlags
3. div (Content's child) completeWork:
flags: NoFlags
bubbleProperties:
- child1 = p("Text 1"): flags = Placement
- child2 = p("Text 2"): flags = Placement
- subtreeFlags = Placement | Placement = Placement
结果:
div.subtreeFlags = Placement
Effect List (旧版):
div.firstEffect = p("Text 1")
p("Text 1").nextEffect = p("Text 2")
div.lastEffect = p("Text 2")
4. Content completeWork:
flags: Update (componentDidMount)
bubbleProperties:
- child = div: subtreeFlags = Placement
- subtreeFlags = Placement
结果:
Content.subtreeFlags = Placement
Content.flags = Update
Effect List (旧版):
Content.firstEffect = p("Text 1")
Content.lastEffect = p("Text 2")
Content 自己加入:
p("Text 2").nextEffect = Content
Content.lastEffect = Content
5. header completeWork:
flags: Placement | Ref
subtreeFlags: NoFlags
6. div (App's child) completeWork:
flags: NoFlags
bubbleProperties:
- child1 = header: flags = Placement | Ref
- child2 = Content: flags = Update, subtreeFlags = Placement
- subtreeFlags = (Placement | Ref) | Update | Placement
结果:
div.subtreeFlags = Placement | Ref | Update
Effect List (旧版):
div.firstEffect = header
header.nextEffect = p("Text 1")
p("Text 1").nextEffect = p("Text 2")
p("Text 2").nextEffect = Content
div.lastEffect = Content
7. App completeWork:
flags: Update (componentDidMount)
bubbleProperties:
- child = div: subtreeFlags = Placement | Ref | Update
- subtreeFlags = Placement | Ref | Update
结果:
App.subtreeFlags = Placement | Ref | Update
App.flags = Update
Effect List (旧版):
App.firstEffect = header
header.nextEffect = p("Text 1")
p("Text 1").nextEffect = p("Text 2")
p("Text 2").nextEffect = Content
Content.nextEffect = App
App.lastEffect = App
8. HostRootFiber completeWork:
flags: NoFlags
bubbleProperties:
- child = App: flags = Update, subtreeFlags = Placement | Ref | Update
- subtreeFlags = Update | Placement | Ref | Update
结果:
HostRootFiber.subtreeFlags = Placement | Ref | Update
Effect List (旧版):
HostRootFiber.firstEffect = header
header.nextEffect = p("Text 1")
p("Text 1").nextEffect = p("Text 2")
p("Text 2").nextEffect = Content
Content.nextEffect = App
HostRootFiber.lastEffect = App
=== 最终的 Effect List(旧版)===
HostRootFiber.firstEffect → header → p1 → p2 → Content → App → null
↓ ↓ ↓ ↓ ↓
Placement Plac Plac Update Update
+ Ref ement ement
=== 最终的 subtreeFlags(新版)===
HostRootFiber: subtreeFlags = Placement | Ref | Update
↓
App: flags = Update, subtreeFlags = Placement | Ref | Update
↓
div: flags = NoFlags, subtreeFlags = Placement | Ref | Update
├─ header: flags = Placement | Ref
└─ Content: flags = Update, subtreeFlags = Placement
↓
div: flags = NoFlags, subtreeFlags = Placement
├─ p1: flags = Placement
└─ p2: flags = Placement
六、Commit 阶段的三个子阶段
6.1 Commit 阶段概述
javascript
// 📍 位置:ReactFiberWorkLoop.new.js
function commitRoot(root) {
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// 重置标记
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 🔥 Commit 阶段的三个子阶段
// 1. Before Mutation 阶段
commitBeforeMutationEffects(root, finishedWork);
// 2. Mutation 阶段
commitMutationEffects(root, finishedWork, lanes);
// 3. Layout 阶段
commitLayoutEffects(finishedWork, root, lanes);
// 切换 current 指针
root.current = finishedWork;
}
6.2 Before Mutation 阶段
javascript
function commitBeforeMutationEffects(root: FiberRoot, firstChild: Fiber) {
/* 💡 Before Mutation 阶段的任务:
* 1. 调用 getSnapshotBeforeUpdate
* 2. 异步调度 useEffect
*/
// 遍历 Effect List(旧版)或 Fiber 树(新版)
let fiber = firstChild;
while (fiber !== null) {
if (fiber.flags & Snapshot) {
// 🔥 调用 getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber(fiber);
}
if (fiber.flags & Passive) {
// 🔥 调度 useEffect
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
});
}
fiber = fiber.nextEffect; // 旧版
// 或遍历子树 // 新版
}
}
function commitBeforeMutationEffectOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case ClassComponent: {
if (flags & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// 🔥 调用 getSnapshotBeforeUpdate
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState
);
// 保存 snapshot,传给 componentDidUpdate
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
break;
}
}
}
6.3 Mutation 阶段
javascript
function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes
) {
/* 💡 Mutation 阶段的任务:
* 1. 执行 DOM 操作(插入、更新、删除)
* 2. 调用 useLayoutEffect 的销毁函数
* 3. 更新 ref
*/
// 遍历 Fiber 树
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
}
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// 🔥 先处理子树
if (finishedWork.subtreeFlags & MutationMask) {
let child = finishedWork.child;
while (child !== null) {
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
// 🔥 再处理自己
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// 调用 useLayoutEffect 的销毁函数
if (flags & Update) {
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
}
break;
}
case HostComponent: {
const instance = finishedWork.stateNode;
if (flags & Placement) {
// 🔥 插入 DOM
const parent = getHostParentFiber(finishedWork);
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNode(finishedWork, before, parent);
}
if (flags & Update) {
// 🔥 更新 DOM 属性
if (instance != null && current !== null) {
const updatePayload = finishedWork.updateQueue;
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
finishedWork.type,
current.memoizedProps,
finishedWork.memoizedProps,
finishedWork
);
}
}
}
if (flags & Ref) {
// 🔥 清空旧 ref
if (current !== null && current.ref !== null) {
commitDetachRef(current);
}
}
break;
}
}
// 🔥 处理删除
if (flags & Deletion) {
const deletions = finishedWork.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
commitDeletionEffects(root, finishedWork, childToDelete);
}
}
}
}
6.4 Layout 阶段
javascript
function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes
) {
/* 💡 Layout 阶段的任务:
* 1. 调用生命周期方法(componentDidMount、componentDidUpdate)
* 2. 调用 useLayoutEffect 的创建函数
* 3. 更新 ref
* 4. 调用 setState 的 callback
*/
// 遍历 Fiber 树
commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
}
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes
) {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case ClassComponent: {
const instance = finishedWork.stateNode;
if (flags & Update) {
if (current === null) {
// 🔥 首次挂载,调用 componentDidMount
instance.componentDidMount();
} else {
// 🔥 更新,调用 componentDidUpdate
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const snapshot = instance.__reactInternalSnapshotBeforeUpdate;
instance.componentDidUpdate(prevProps, prevState, snapshot);
}
}
if (flags & Callback) {
// 🔥 调用 setState 的 callback
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
commitUpdateQueue(finishedWork, updateQueue, instance);
}
}
break;
}
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (flags & Update) {
// 🔥 调用 useLayoutEffect 的创建函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
break;
}
case HostComponent: {
if (flags & Ref) {
// 🔥 更新 ref
commitAttachRef(finishedWork);
}
break;
}
}
// 递归处理子树
if (finishedWork.subtreeFlags & LayoutMask) {
let child = finishedWork.child;
while (child !== null) {
commitLayoutEffectOnFiber(
finishedRoot,
child.alternate,
child,
committedLanes
);
child = child.sibling;
}
}
}
七、Effect List 的遍历和消费
7.1 旧版的遍历方式
javascript
// 📍 位置:ReactFiberWorkLoop.old.js(React <= 17)
function commitRootImpl(root, renderPriorityLevel) {
const finishedWork = root.finishedWork;
// 获取 Effect List
let firstEffect = finishedWork.firstEffect;
if (firstEffect !== null) {
// ========== Before Mutation 阶段 ==========
let nextEffect = firstEffect;
do {
try {
commitBeforeMutationEffects();
} catch (error) {
// 错误处理
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
// ========== Mutation 阶段 ==========
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
// 切换 current 树
root.current = finishedWork;
// ========== Layout 阶段 ==========
nextEffect = firstEffect;
do {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
// 清空 nextEffect
nextEffect = null;
}
}
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
// 处理 getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
if ((flags & Passive) !== NoFlags) {
// 调度 useEffect
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
});
}
nextEffect = nextEffect.nextEffect;
}
}
7.2 新版的遍历方式
javascript
// 📍 位置:ReactFiberWorkLoop.new.js(React >= 18)
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority
) {
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// ========== Before Mutation 阶段 ==========
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork
);
// ========== Mutation 阶段 ==========
commitMutationEffects(root, finishedWork, lanes);
// 切换 current 树
root.current = finishedWork;
// ========== Layout 阶段 ==========
commitLayoutEffects(finishedWork, root, lanes);
// 请求绘制
requestPaint();
}
// 新版不使用 Effect List,而是递归遍历 Fiber 树
function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes
) {
// 递归遍历整个 Fiber 树
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
}
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// 🔥 优化:先检查 subtreeFlags
if (finishedWork.subtreeFlags & MutationMask) {
// 子树有副作用,递归处理
let child = finishedWork.child;
while (child !== null) {
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
// 🔥 处理自己的副作用
if (flags & MutationMask) {
// 执行 DOM 操作
switch (finishedWork.tag) {
case HostComponent:
if (flags & Placement) {
commitPlacement(finishedWork);
}
if (flags & Update) {
commitWork(current, finishedWork);
}
// ...
}
}
}
7.3 两种方式的对比
javascript
// ========== 旧版(Effect List)==========
// 优点:
// 1. 只遍历有副作用的节点,性能更好
// 2. 跳过无副作用的子树
// 缺点:
// 1. 需要维护额外的链表结构
// 2. 增加了 Render 阶段的复杂度
// 3. 占用更多内存(firstEffect、lastEffect、nextEffect 指针)
let effect = root.firstEffect;
while (effect !== null) {
// 只处理有副作用的节点
commitWork(effect);
effect = effect.nextEffect;
}
// ========== 新版(subtreeFlags)==========
// 优点:
// 1. 不需要额外的链表结构
// 2. Render 阶段更简单
// 3. 节省内存
// 缺点:
// 1. 需要递归遍历 Fiber 树
// 2. 可能会访问一些无副作用的节点(但可以通过 subtreeFlags 剪枝)
function commitWork(fiber) {
// 先检查是否需要处理子树
if (fiber.subtreeFlags & SomeMask) {
let child = fiber.child;
while (child !== null) {
commitWork(child); // 递归
child = child.sibling;
}
}
// 处理自己
if (fiber.flags & SomeMask) {
// 执行副作用
}
}
八、完整的 Commit 流程示例
8.1 示例:状态更新
jsx
class App extends React.Component {
state = { count: 0 };
headerRef = React.createRef();
componentDidMount() {
console.log("App mounted");
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("App updated", snapshot);
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("Before update", prevState.count, "->", this.state.count);
return { prevCount: prevState.count };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<header ref={this.headerRef}>Count: {this.state.count}</header>
<button onClick={this.handleClick}>+1</button>
</div>
);
}
}
8.2 点击按钮后的完整流程
markdown
=== 触发更新 ===
1. 点击 button
2. 调用 handleClick
3. 执行 setState({ count: 1 })
4. 创建 update 对象,放入 updateQueue
5. 调度更新
=== Render 阶段 ===
1. beginWork(AppFiber)
- processUpdateQueue: 计算新 state {count: 1}
- instance.render()
- reconcileChildren
- 发现 header 的 props 变化:{children: "Count: 0"} → {children: "Count: 1"}
- headerFiber.flags |= Update
2. beginWork(headerFiber)
- 文本变化,需要更新
- 继续...
3. completeWork(headerFiber)
- headerFiber.flags = Update
- 创建 updatePayload: ["children", "Count: 1"]
- headerFiber.updateQueue = updatePayload
4. completeWork(divFiber)
- bubbleProperties:
- child1(header).flags = Update
- subtreeFlags = Update
- divFiber.subtreeFlags = Update
5. completeWork(AppFiber)
- AppFiber.flags = Update | Snapshot
- Update: componentDidUpdate
- Snapshot: getSnapshotBeforeUpdate
- bubbleProperties:
- child(div).subtreeFlags = Update
- subtreeFlags = Update
- AppFiber.subtreeFlags = Update
6. Render 阶段完成
- HostRootFiber.subtreeFlags = Update | Snapshot
=== Commit 阶段 ===
// Before Mutation 阶段:
1. 遍历到 AppFiber:
- flags & Snapshot ✓
- 调用 getSnapshotBeforeUpdate(prevProps, prevState)
- 控制台输出:"Before update 0 -> 1"
- 保存 snapshot: instance.__reactInternalSnapshotBeforeUpdate = {prevCount: 0}
// Mutation 阶段:
1. 切换到 HostRootFiber
2. 递归到 divFiber:
- divFiber.subtreeFlags & MutationMask ✓
- 继续递归
3. 递归到 headerFiber:
- headerFiber.flags & Update ✓
- commitWork(headerFiber):
- updatePayload = ["children", "Count: 1"]
- updateDOMProperties(headerDOM, updatePayload)
- headerDOM.textContent = "Count: 1"
- 🔥 DOM 已更新!
4. 切换 current 指针:
- root.current = finishedWork
- 新的 Fiber 树成为 current 树
// Layout 阶段:
1. 递归到 AppFiber:
- flags & Update ✓
- 调用 componentDidUpdate:
- prevProps: {}
- prevState: {count: 0}
- snapshot: {prevCount: 0}
- 控制台输出:"App updated {prevCount: 0}"
2. 递归到 headerFiber:
- flags & Ref ✓
- commitAttachRef(headerFiber):
- this.headerRef.current = headerDOM
=== Commit 完成 ===
- 屏幕显示 "Count: 1"
- 控制台输出:
"Before update 0 -> 1"
"App updated {prevCount: 0}"
九、新旧版本的差异
9.1 数据结构的变化
javascript
// ========== React 17 及之前 ==========
type Fiber = {
// Effect List 相关
firstEffect: Fiber | null, // 子树 Effect List 的头
lastEffect: Fiber | null, // 子树 Effect List 的尾
nextEffect: Fiber | null, // 下一个有副作用的节点
// 副作用标记
flags: Flags, // 自身的副作用
// 其他字段...
};
// Effect List 的结构:
root.firstEffect → fiber1 → fiber2 → fiber3 → null
↓ ↓ ↓
nextEffect nextEffect nextEffect
// ========== React 18 及之后 ==========
type Fiber = {
// 移除了 Effect List
// firstEffect: 删除
// lastEffect: 删除
// nextEffect: 删除
// 新增 subtreeFlags
flags: Flags, // 自身的副作用
subtreeFlags: Flags, // 子树的副作用(子节点 flags 的并集)
// 其他字段...
};
// 不再使用链表,直接遍历 Fiber 树
9.2 构建方式的变化
javascript
// ========== React 17 及之前 ==========
function completeUnitOfWork(unitOfWork) {
// ...
if (returnFiber !== null) {
// 🔥 手动构建 Effect List
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
}
// ========== React 18 及之后 ==========
function bubbleProperties(completedWork) {
// 🔥 只需要收集 flags
let subtreeFlags = NoFlags;
let child = completedWork.child;
while (child !== null) {
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags;
}
9.3 消费方式的变化
javascript
// ========== React 17 及之前 ==========
function commitMutationEffects() {
// 🔥 遍历 Effect List
while (nextEffect !== null) {
const flags = nextEffect.flags;
if (flags & Placement) {
commitPlacement(nextEffect);
}
if (flags & Update) {
commitWork(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
// ========== React 18 及之后 ==========
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
// 🔥 递归遍历 Fiber 树
// 先处理子树
if (finishedWork.subtreeFlags & MutationMask) {
let child = finishedWork.child;
while (child !== null) {
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
// 再处理自己
const flags = finishedWork.flags;
if (flags & Placement) {
commitPlacement(finishedWork);
}
if (flags & Update) {
commitWork(current, finishedWork);
}
}
十、常见问题深度解答
Q1: 为什么要从 Effect List 改成 subtreeFlags?
深度解答:
javascript
// Effect List 的问题:
1. 内存开销
- 每个 Fiber 需要 3 个额外指针:firstEffect、lastEffect、nextEffect
- 假设 10000 个 Fiber,就是 30000 个指针
- 每个指针 8 字节,总共 240KB
2. 构建复杂度
- 需要在 completeUnitOfWork 中手动维护链表
- 代码逻辑复杂,容易出错
3. 不利于并发模式
- Effect List 是全局的、线性的
- 在并发模式中,可能需要中断和恢复
- Effect List 的维护变得更加困难
// subtreeFlags 的优势:
1. 更少的内存
- 只需要一个 subtreeFlags 字段(32 位整数)
- 10000 个 Fiber 只需要 40KB
2. 更简单的实现
- 只需要按位或操作:subtreeFlags |= child.flags
- 代码更简洁,更不容易出错
3. 更好的并发支持
- 可以随时中断和恢复
- 不需要维护全局的链表状态
4. 更灵活的优化
- 可以根据 subtreeFlags 决定是否需要遍历子树
- 实现更细粒度的剪枝
// 性能对比:
// Effect List(旧版)
// 优点:只访问有副作用的节点
// 缺点:构建链表有开销
let effect = root.firstEffect;
let count = 0;
while (effect !== null) {
count++;
effect = effect.nextEffect;
}
// 假设有 100 个有副作用的节点,访问 100 次
// subtreeFlags(新版)
// 优点:构建简单,内存小
// 缺点:可能访问一些无副作用的节点
function traverse(fiber, count = 0) {
if (fiber.subtreeFlags & SomeMask) {
let child = fiber.child;
while (child !== null) {
count = traverse(child, count);
child = child.sibling;
}
}
if (fiber.flags & SomeMask) {
count++;
}
return count;
}
// 假设 Fiber 树有 10000 个节点,但只有 100 个有副作用
// 最坏情况:访问所有 10000 个节点
// 最好情况:通过 subtreeFlags 剪枝,只访问几百个节点
// 实际测试表明:
// - 构建 Effect List 的开销 > subtreeFlags 的遍历开销
// - 内存节省显著
// - 代码简洁性提升
// - 因此 React 18 选择了 subtreeFlags
Q2: Commit 阶段为什么要分三个子阶段?
深度解答:
javascript
// 三个阶段的必要性:
// ========== Before Mutation 阶段 ==========
// 时机:DOM 变更之前
// 作用:
// 1. 调用 getSnapshotBeforeUpdate
// - 可以读取变更前的 DOM 状态
// - 例如:保存滚动位置
// 2. 调度 useEffect
// - 异步执行,不阻塞渲染
class ScrollList extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
// 🔥 在 DOM 更新前,读取滚动位置
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 🔥 在 DOM 更新后,恢复滚动位置
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
}
// ========== Mutation 阶段 ==========
// 时机:执行 DOM 变更
// 作用:
// 1. 插入、更新、删除 DOM
// 2. 调用 useLayoutEffect 的销毁函数
// - 需要在 DOM 变更时执行
function Example() {
const [width, setWidth] = useState(0);
const ref = useRef();
useLayoutEffect(() => {
// 🔥 在 DOM 更新后,重新测量
setWidth(ref.current.offsetWidth);
return () => {
// 🔥 在 DOM 更新前,清理
console.log('cleanup');
};
});
return <div ref={ref}>Width: {width}</div>;
}
// ========== Layout 阶段 ==========
// 时机:DOM 变更之后,浏览器绘制之前
// 作用:
// 1. 调用 componentDidMount/componentDidUpdate
// 2. 调用 useLayoutEffect 的创建函数
// 3. 更新 ref.current
// 4. 调用 setState 的 callback
// 为什么要在 Layout 阶段更新 ref?
class Parent extends React.Component {
childRef = React.createRef();
componentDidMount() {
// 🔥 此时 ref 已经更新
console.log(this.childRef.current); // <div>Child</div>
}
render() {
return <Child ref={this.childRef} />;
}
}
// 如果只有一个阶段会怎样?
// 错误示例:所有操作在一个阶段
commitEffects() {
// 1. 更新 DOM
updateDOM();
// 2. 调用 getSnapshotBeforeUpdate
// ❌ 太晚了!DOM 已经更新了
getSnapshotBeforeUpdate();
// 3. 调用 componentDidUpdate
componentDidUpdate();
}
// 正确示例:分三个阶段
commitBeforeMutation() {
// ✅ DOM 更新前调用
getSnapshotBeforeUpdate();
}
commitMutation() {
// ✅ 更新 DOM
updateDOM();
}
commitLayout() {
// ✅ DOM 更新后调用
componentDidUpdate();
}
Q3: 副作用的执行顺序是怎样的?
深度解答:
javascript
// 示例代码:
class Parent extends React.Component {
componentDidMount() {
console.log("Parent didMount");
}
componentDidUpdate() {
console.log("Parent didUpdate");
}
render() {
return (
<div>
<Child1 />
<Child2 />
</div>
);
}
}
class Child1 extends React.Component {
componentDidMount() {
console.log("Child1 didMount");
}
render() {
return <div>Child1</div>;
}
}
class Child2 extends React.Component {
componentDidMount() {
console.log("Child2 didMount");
}
render() {
return <div>Child2</div>;
}
}
// ========== 首次渲染的执行顺序 ==========
// Render 阶段(深度优先遍历):
// 1. Parent beginWork
// 2. div beginWork
// 3. Child1 beginWork
// 4. Child1 completeWork → 标记 flags
// 5. Child2 beginWork
// 6. Child2 completeWork → 标记 flags
// 7. div completeWork → 收集子树 flags
// 8. Parent completeWork → 收集子树 flags
// Effect List(旧版):
// Child1 → Child2 → Parent
// Commit 阶段(按 Effect List 顺序):
// 输出:
// "Child1 didMount"
// "Child2 didMount"
// "Parent didMount"
// 🔥 结论:子组件的生命周期先执行,父组件后执行
// ========== 更新时的执行顺序 ==========
// 假设更新 Parent 的 state
// Render 阶段:
// 1. Parent beginWork → 重新 render
// 2. div beginWork
// 3. Child1 beginWork → bailout(props 没变)
// 4. Child2 beginWork → bailout(props 没变)
// 5. 回溯,收集 flags
// Effect List:
// Parent
// Commit 阶段:
// 输出:
// "Parent didUpdate"
// 🔥 结论:只有 Parent 有副作用,只执行 Parent 的生命周期
// ========== useEffect 的执行顺序 ==========
function Parent() {
useEffect(() => {
console.log("Parent effect");
return () => console.log("Parent cleanup");
});
return (
<div>
<Child1 />
<Child2 />
</div>
);
}
function Child1() {
useEffect(() => {
console.log("Child1 effect");
return () => console.log("Child1 cleanup");
});
return <div>Child1</div>;
}
function Child2() {
useEffect(() => {
console.log("Child2 effect");
return () => console.log("Child2 cleanup");
});
return <div>Child2</div>;
}
// 首次渲染:
// Commit 阶段(Layout 阶段后,异步执行):
// 输出:
// "Child1 effect"
// "Child2 effect"
// "Parent effect"
// 更新时:
// 输出:
// "Child1 cleanup"
// "Child2 cleanup"
// "Parent cleanup"
// "Child1 effect"
// "Child2 effect"
// "Parent effect"
// 🔥 结论:
// 1. 创建函数:子组件先执行,父组件后执行
// 2. 销毁函数:也是子组件先执行,父组件后执行
// 3. 销毁函数在创建函数之前执行
// ========== useLayoutEffect vs useEffect ==========
function Example() {
useLayoutEffect(() => {
console.log("useLayoutEffect");
});
useEffect(() => {
console.log("useEffect");
});
return <div>Example</div>;
}
// 执行顺序:
// 1. Commit Mutation 阶段:更新 DOM
// 2. Commit Layout 阶段:useLayoutEffect
// 3. 浏览器绘制
// 4. 异步执行:useEffect
// 输出:
// "useLayoutEffect"
// (浏览器绘制)
// "useEffect"
// 🔥 结论:
// - useLayoutEffect 在浏览器绘制前执行(同步)
// - useEffect 在浏览器绘制后执行(异步)
十一、总结
11.1 Effect List 的核心要点
旧版(React <= 17):
- 构建:在 completeUnitOfWork 中手动构建链表
- 结构:firstEffect → effect1 → effect2 → ... → lastEffect
- 消费:遍历链表,执行副作用
- 优点:只访问有副作用的节点
- 缺点:额外的内存开销,维护复杂
新版(React >= 18):
- 构建:在 bubbleProperties 中收集 subtreeFlags
- 结构:每个节点有 flags 和 subtreeFlags
- 消费:递归遍历 Fiber 树,根据 flags 判断
- 优点:内存小,代码简单,支持并发
- 缺点:可能访问一些无副作用的节点(但可以剪枝)
11.2 Commit 阶段的流程
sql
Commit 阶段
↓
Before Mutation 阶段
├─ 调用 getSnapshotBeforeUpdate
└─ 调度 useEffect(异步)
↓
Mutation 阶段
├─ 执行 DOM 操作(Placement、Update、Deletion)
└─ 调用 useLayoutEffect 的销毁函数
↓
切换 current 指针
↓
Layout 阶段
├─ 调用 componentDidMount/componentDidUpdate
├─ 调用 useLayoutEffect 的创建函数
├─ 更新 ref.current
└─ 调用 setState 的 callback
↓
请求浏览器绘制
↓
异步执行 useEffect
11.3 关键记忆点
- 🔥 副作用在 completeWork 阶段收集(归阶段)
- 🔥 旧版使用 Effect List 链表 ,新版使用 subtreeFlags
- 🔥 Commit 阶段分为 三个子阶段(Before Mutation、Mutation、Layout)
- 🔥 生命周期执行顺序:子组件先执行,父组件后执行
- 🔥 useLayoutEffect 同步执行 ,useEffect 异步执行
- 🔥 DOM 操作在 Mutation 阶段执行