Appearance
第12章 提交阶段
本章将深入React的commit阶段,理解如何将render阶段计算出的变更应用到真实DOM,以及Effect的执行时机和顺序。这是React渲染流程的最后一个关键阶段。
在前面的章节中,我们学习了React的协调过程(Reconciliation)。render阶段通过Diff算法找出了需要执行的DOM操作,并标记在Fiber节点的flags上。但是,这些操作还没有真正应用到DOM上。
这就是commit阶段的职责。commit阶段负责将render阶段的计算结果应用到真实DOM,执行副作用(Effect),更新ref,调用生命周期方法等。
为什么React要将渲染分为render和commit两个阶段?commit阶段为什么不能中断?before mutation、mutation、layout三个子阶段分别做什么?useEffect和useLayoutEffect有什么区别?
本章将逐一解答这些问题,带你深入理解React commit阶段的设计与实现。
12.1 commit阶段概述
commit阶段是React渲染流程的最后一个阶段,它将render阶段计算出的变更应用到真实DOM。
12.1.1 commitRoot入口
当render阶段完成后,React会调用commitRoot函数进入commit阶段:
javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:2100-2150(React 19.3.0)
function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
) {
// 1. 获取当前的优先级
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
// 2. 设置为最高优先级(DiscreteEventPriority)
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(DiscreteEventPriority);
// 3. 执行commit工作
commitRootImpl(
root,
recoverableErrors,
previousUpdateLanePriority,
);
} finally {
// 4. 恢复优先级
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}为什么commit阶段使用最高优先级?
commit阶段必须同步执行,不能被打断。因为:
- DOM操作的原子性:部分更新会导致页面状态不一致
- 用户体验:避免用户看到中间状态
- 副作用的顺序:Effect必须按照特定顺序执行
12.1.2 三个子阶段
commit阶段分为三个子阶段,每个子阶段处理不同类型的副作用:
javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:2200-2400(React 19.3.0)
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
renderPriorityLevel: EventPriority,
) {
// 获取finishedWork(render阶段完成的Fiber树)
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
// 清空finishedWork和finishedLanes
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 保存当前的Fiber树
root.callbackNode = null;
root.callbackPriority = NoLane;
// ===== 第一阶段:before mutation =====
// 在DOM变更之前执行
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
// ===== 第二阶段:mutation =====
// 执行DOM变更
commitMutationEffects(root, finishedWork, lanes);
// 切换current树
root.current = finishedWork;
// ===== 第三阶段:layout =====
// 在DOM变更之后执行
commitLayoutEffects(finishedWork, root, lanes);
// 请求绘制
requestPaint();
// 调度useEffect
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
// 检查是否还有其他工作
ensureRootIsScheduled(root);
return null;
}三个子阶段的职责
| 阶段 | 时机 | 主要工作 |
|---|---|---|
| before mutation | DOM变更前 | getSnapshotBeforeUpdate、调度useEffect |
| mutation | DOM变更中 | 插入、更新、删除DOM节点 |
| layout | DOM变更后 | useLayoutEffect、ref更新、生命周期 |
为什么需要三个子阶段?
before mutation:在DOM变更前获取快照
- 例如:获取滚动位置、焦点状态
- 调度useEffect(异步执行)
mutation:执行DOM操作
- 这是唯一修改DOM的阶段
- 操作完成后切换current树
layout:在DOM变更后执行同步副作用
- useLayoutEffect可以读取最新的DOM
- ref可以获取最新的DOM引用
- 生命周期方法可以访问最新的DOM
current树的切换时机
注意,current树的切换发生在mutation阶段之后、layout阶段之前:
before mutation阶段:
- root.current指向旧的Fiber树
- 可以访问旧的DOM状态
mutation阶段:
- root.current仍指向旧的Fiber树
- 正在修改DOM
切换current树:
- root.current = finishedWork
- 现在指向新的Fiber树
layout阶段:
- root.current指向新的Fiber树
- 可以访问新的DOM状态这样设计的好处:
- before mutation阶段可以访问旧的DOM状态
- layout阶段可以访问新的DOM状态
- 保证了状态的一致性
12.2 before mutation阶段
before mutation阶段在DOM变更之前执行,主要负责调用getSnapshotBeforeUpdate生命周期和调度useEffect。
12.2.1 commitBeforeMutationEffects
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:300-400(React 19.3.0)
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
): boolean {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
// 返回是否需要在mutation后触发blur事件
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// 处理子节点
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
child.return = fiber;
nextEffect = child;
} else {
commitBeforeMutationEffects_complete();
}
}
}
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
try {
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}遍历模式
before mutation阶段使用深度优先遍历,但只处理有BeforeMutationMask flags的节点:
Fiber树:
App
/ \
Header Main (有Snapshot flag)
/ / \
Logo List Sidebar
/ \
Item1 Item2
遍历过程:
1. App:检查subtreeFlags,有BeforeMutationMask,向下
2. Header:检查subtreeFlags,没有BeforeMutationMask,跳过子树
3. Main:有Snapshot flag,执行commitBeforeMutationEffectsOnFiber
4. 继续遍历其他节点...12.2.2 调度useEffect
before mutation阶段的一个重要工作是调度useEffect:
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:500-600(React 19.3.0)
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// 处理useEffect
if ((flags & Passive) !== NoFlags) {
// 标记有Passive effect需要执行
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
}
}
break;
}
case ClassComponent: {
if ((flags & Snapshot) !== NoFlags) {
if (current !== null) {
// 调用getSnapshotBeforeUpdate
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
// 保存snapshot,供componentDidUpdate使用
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
break;
}
case HostRoot: {
if ((flags & Snapshot) !== NoFlags) {
// 清空容器(首次渲染时)
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
break;
}
}
}getSnapshotBeforeUpdate的使用场景
getSnapshotBeforeUpdate在DOM变更前被调用,可以获取变更前的DOM状态:
jsx
class ChatThread extends React.Component {
chatThreadRef = React.createRef();
getSnapshotBeforeUpdate(prevProps, prevState) {
// 在新消息添加前,保存滚动位置
if (prevProps.messages.length < this.props.messages.length) {
const chat = this.chatThreadRef.current;
return chat.scrollHeight - chat.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果有新消息,保持滚动位置
if (snapshot !== null) {
const chat = this.chatThreadRef.current;
chat.scrollTop = chat.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.chatThreadRef}>
{this.props.messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
}
}为什么在before mutation阶段调度useEffect?
useEffect是异步执行的,需要在commit阶段开始时就调度,这样可以:
- 尽早调度:不用等到layout阶段
- 不阻塞渲染:useEffect异步执行,不会阻塞浏览器绘制
- 保证顺序:在mutation和layout之前调度,保证执行顺序
12.3 mutation阶段
mutation阶段是commit阶段的核心,负责执行所有的DOM操作。
12.3.1 Placement插入操作
Placement flag表示需要插入新的DOM节点:
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1500-1600(React 19.3.0)
function commitPlacement(finishedWork: Fiber): void {
// 1. 找到父DOM节点
const parentFiber = getHostParentFiber(finishedWork);
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
default:
throw new Error('Invalid host parent fiber.');
}
// 2. 如果父节点有ContentReset flag,清空文本内容
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}
// 3. 找到插入位置(before节点)
const before = getHostSibling(finishedWork);
// 4. 执行插入操作
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}getHostSibling(查找插入位置)
这是commit阶段最复杂的函数之一,用于找到正确的插入位置:
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1200-1400(React 19.3.0)
function getHostSibling(fiber: Fiber): Instance | null {
// 查找下一个Host类型的兄弟节点
let node: Fiber = fiber;
siblings: while (true) {
// 向上查找,直到找到有兄弟节点的节点
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// 没有兄弟节点
return null;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
// 向下查找第一个Host类型的节点
while (node.tag !== HostComponent && node.tag !== HostText) {
// 如果这个节点也是新插入的,跳过
if (node.flags & Placement) {
continue siblings;
}
// 如果没有子节点,继续查找兄弟节点
if (node.child === null) {
continue siblings;
} else {
node.child.return = node;
node = node.child;
}
}
// 找到了Host节点,且不是新插入的
if (!(node.flags & Placement)) {
return node.stateNode;
}
}
}为什么查找插入位置这么复杂?
因为Fiber树和DOM树的结构不一致:
jsx
// React组件树
<div>
<List>
<Item /> {/* 新插入 */}
</List>
<span>text</span>
</div>
// Fiber树
div
├── List (FunctionComponent)
│ └── Item (HostComponent) [Placement]
└── span (HostComponent)
// DOM树
div
├── Item (需要插入到span前面)
└── spangetHostSibling需要跳过组件节点,找到真正的DOM兄弟节点。
insertOrAppendPlacementNode(执行插入)
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
function insertOrAppendPlacementNode(
node: Fiber,
before: Instance | null,
parent: Instance,
): void {
const tag = node.tag;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
// 是Host节点,直接插入
const stateNode = node.stateNode;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else {
// 不是Host节点,递归处理子节点
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}12.3.2 Update更新操作
Update flag表示需要更新DOM节点的属性:
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1700-1800(React 19.3.0)
function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// 更新Fiber引用
updateFiberProps(domElement, newProps);
// 更新DOM属性
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}updateProperties(更新DOM属性)
javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastProps: Object,
nextProps: Object,
): void {
// updatePayload格式:[key1, value1, key2, value2, ...]
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === 'style') {
// 更新style
setValueForStyles(domElement, propValue);
} else if (propKey === 'dangerouslySetInnerHTML') {
// 更新innerHTML
setInnerHTML(domElement, propValue);
} else if (propKey === 'children') {
// 更新文本内容
setTextContent(domElement, propValue);
} else {
// 更新其他属性
setValueForProperty(domElement, propKey, propValue);
}
}
// 处理特殊标签的后续逻辑
switch (tag) {
case 'input':
// 更新input的value
updateInput(domElement, nextProps);
break;
case 'textarea':
// 更新textarea的value
updateTextarea(domElement, nextProps);
break;
case 'select':
// 更新select的value
updateSelect(domElement, nextProps);
break;
}
}示例:更新DOM属性
jsx
// 旧props
<div className="old" style={{ color: 'red' }}>
Old Text
</div>
// 新props
<div className="new" style={{ color: 'blue', fontSize: '14px' }}>
New Text
</div>
// updatePayload
[
'className', 'new',
'style', { color: 'blue', fontSize: '14px' },
'children', 'New Text',
]
// 执行更新
domElement.className = 'new';
domElement.style.color = 'blue';
domElement.style.fontSize = '14px';
domElement.textContent = 'New Text';12.3.3 Deletion删除操作
Deletion flag表示需要删除DOM节点:
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1900-2100(React 19.3.0)
function commitDeletion(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
): void {
// 1. 卸载ref
// 2. 调用componentWillUnmount
// 3. 执行useEffect cleanup
// 4. 删除DOM节点
unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
}
function unmountHostComponents(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
): void {
let node: Fiber = current;
// 深度优先遍历,卸载所有节点
let currentParentIsValid = false;
let currentParent;
let currentParentIsContainer;
while (true) {
if (!currentParentIsValid) {
// 查找父DOM节点
let parent = node.return;
findParent: while (true) {
const parentStateNode = parent.stateNode;
switch (parent.tag) {
case HostComponent:
currentParent = parentStateNode;
currentParentIsContainer = false;
break findParent;
case HostRoot:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case HostPortal:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
}
parent = parent.return;
}
currentParentIsValid = true;
}
if (node.tag === HostComponent || node.tag === HostText) {
// 递归卸载子节点
commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor);
// 删除DOM节点
if (currentParentIsContainer) {
removeChildFromContainer(currentParent, node.stateNode);
} else {
removeChild(currentParent, node.stateNode);
}
} else {
// 处理其他类型的节点
commitUnmount(finishedRoot, node, nearestMountedAncestor);
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
if (node === current) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === current) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}commitUnmount(卸载节点)
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
function commitUnmount(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
): void {
switch (current.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// 执行useEffect cleanup
const updateQueue = current.updateQueue;
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const destroy = effect.destroy;
if (destroy !== undefined) {
safelyCallDestroy(current, nearestMountedAncestor, destroy);
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
break;
}
case ClassComponent: {
// 卸载ref
safelyDetachRef(current, nearestMountedAncestor);
// 调用componentWillUnmount
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(
current,
nearestMountedAncestor,
instance,
);
}
break;
}
case HostComponent: {
// 卸载ref
safelyDetachRef(current, nearestMountedAncestor);
break;
}
}
}删除操作的顺序
删除节点时,需要按照特定的顺序执行:
1. 递归处理子节点
- 执行useEffect cleanup
- 调用componentWillUnmount
- 卸载ref
2. 删除DOM节点
- 从父节点移除
3. 清理Fiber节点
- 断开引用
- 释放内存示例:
jsx
function Parent() {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(false)}>Hide</button>
{show && <Child />}
</div>
);
}
function Child() {
useEffect(() => {
console.log('Child mounted');
return () => {
console.log('Child cleanup'); // 先执行
};
}, []);
return <div>Child</div>; // 后删除DOM
}
// 点击Hide按钮时的执行顺序:
// 1. Child cleanup(useEffect cleanup)
// 2. 删除<div>Child</div>(DOM删除)12.4 layout阶段
layout阶段在DOM变更之后执行,主要负责执行useLayoutEffect、更新ref、调用生命周期方法。
12.4.1 useLayoutEffect执行
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:400-500(React 19.3.0)
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
nextEffect = finishedWork;
commitLayoutEffects_begin(finishedWork, root, committedLanes);
}
function commitLayoutEffects_begin(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
try {
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}commitLayoutEffectOnFiber(执行layout副作用)
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// 执行useLayoutEffect
if ((flags & (Update | Callback)) !== NoFlags) {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
break;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (flags & Update) {
if (current === null) {
// 首次渲染:调用componentDidMount
instance.componentDidMount();
} else {
// 更新:调用componentDidUpdate
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
// 获取snapshot(来自getSnapshotBeforeUpdate)
const snapshot = instance.__reactInternalSnapshotBeforeUpdate;
instance.componentDidUpdate(prevProps, prevState, snapshot);
}
}
// 执行setState的callback
if (flags & Callback) {
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
commitUpdateQueue(finishedWork, updateQueue, instance);
}
}
break;
}
case HostRoot: {
// 执行ReactDOM.render的callback
if (flags & Callback) {
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
instance = finishedWork.child.stateNode;
break;
}
}
commitUpdateQueue(finishedWork, updateQueue, instance);
}
}
break;
}
case HostComponent: {
// 处理autoFocus
const instance = finishedWork.stateNode;
if (current === null && flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
commitMount(instance, type, props, finishedWork);
}
break;
}
}
}commitHookEffectListMount(执行useLayoutEffect)
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
function commitHookEffectListMount(
flags: HookFlags,
finishedWork: Fiber,
) {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// 执行useLayoutEffect的create函数
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}useLayoutEffect vs useEffect
jsx
function Component() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
console.log('useLayoutEffect', count);
return () => {
console.log('useLayoutEffect cleanup', count);
};
}, [count]);
useEffect(() => {
console.log('useEffect', count);
return () => {
console.log('useEffect cleanup', count);
};
}, [count]);
return <div>{count}</div>;
}
// 执行顺序:
// 1. render阶段
// 2. before mutation阶段(调度useEffect)
// 3. mutation阶段(更新DOM)
// 4. layout阶段
// - useLayoutEffect cleanup(同步)
// - useLayoutEffect(同步)
// 5. commit阶段完成
// 6. 浏览器绘制
// 7. useEffect cleanup(异步)
// 8. useEffect(异步)| 特性 | useLayoutEffect | useEffect |
|---|---|---|
| 执行时机 | DOM变更后,浏览器绘制前 | 浏览器绘制后 |
| 是否阻塞渲染 | 是(同步) | 否(异步) |
| 适用场景 | 需要读取DOM布局、同步更新DOM | 数据获取、订阅、日志 |
| 性能影响 | 可能阻塞渲染 | 不阻塞渲染 |
useLayoutEffect的使用场景
jsx
// 场景1:测量DOM尺寸
function Tooltip() {
const [tooltipHeight, setTooltipHeight] = useState(0);
const ref = useRef(null);
useLayoutEffect(() => {
// 在浏览器绘制前测量高度,避免闪烁
const height = ref.current.getBoundingClientRect().height;
setTooltipHeight(height);
}, []);
return <div ref={ref} style={{ top: -tooltipHeight }}>Tooltip</div>;
}
// 场景2:同步更新DOM
function ScrollToTop() {
useLayoutEffect(() => {
// 在浏览器绘制前滚动到顶部,避免看到滚动过程
window.scrollTo(0, 0);
}, []);
return <div>Content</div>;
}12.4.2 ref的绑定
layout阶段还负责更新ref:
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
// DOM节点
instanceToUse = getPublicInstance(instance);
break;
default:
// 类组件实例
instanceToUse = instance;
}
if (typeof ref === 'function') {
// ref callback
ref(instanceToUse);
} else {
// ref object
ref.current = instanceToUse;
}
}
}
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
// ref callback
currentRef(null);
} else {
// ref object
currentRef.current = null;
}
}
}ref的更新时机
jsx
function Component() {
const ref = useRef(null);
useLayoutEffect(() => {
// 此时ref.current已经指向DOM节点
console.log(ref.current); // <div>Content</div>
}, []);
useEffect(() => {
// 此时ref.current也已经指向DOM节点
console.log(ref.current); // <div>Content</div>
}, []);
return <div ref={ref}>Content</div>;
}
// 执行顺序:
// 1. mutation阶段:创建/更新DOM
// 2. layout阶段:
// - 更新ref(ref.current = DOM节点)
// - 执行useLayoutEffect(可以访问ref.current)
// 3. 浏览器绘制
// 4. 执行useEffect(可以访问ref.current)ref callback的使用
jsx
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
// ref callback在layout阶段执行
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}12.5 Effect的异步调度
useEffect是异步执行的,在commit阶段完成后才会执行。
12.5.1 flushPassiveEffects
在before mutation阶段,React会调度flushPassiveEffects:
javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:2500-2600(React 19.3.0)
function flushPassiveEffects(): boolean {
if (rootWithPendingPassiveEffects !== null) {
const root = rootWithPendingPassiveEffects;
const lanes = pendingPassiveEffectsLanes;
rootWithPendingPassiveEffects = null;
pendingPassiveEffectsLanes = NoLanes;
// 执行useEffect
return flushPassiveEffectsImpl();
}
return false;
}
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
const root = rootWithPendingPassiveEffects;
rootWithPendingPassiveEffects = null;
// 1. 执行所有useEffect的cleanup函数
commitPassiveUnmountEffects(root.current);
// 2. 执行所有useEffect的create函数
commitPassiveMountEffects(root, root.current);
return true;
}commitPassiveUnmountEffects(执行cleanup)
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveUnmountEffects(finishedWork: Fiber): void {
setCurrentDebugFiberInDEV(finishedWork);
commitPassiveUnmountEffects_begin();
setCurrentDebugFiberInDEV(null);
}
function commitPassiveUnmountEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
const child = fiber.child;
if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) {
child.return = fiber;
nextEffect = child;
} else {
commitPassiveUnmountEffects_complete();
}
}
}
function commitPassiveUnmountEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & Passive) !== NoFlags) {
commitPassiveUnmountOnFiber(fiber);
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// 执行useEffect cleanup
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
break;
}
}
}commitPassiveMountEffects(执行create)
javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveMountEffects(
root: FiberRoot,
finishedWork: Fiber,
): void {
setCurrentDebugFiberInDEV(finishedWork);
commitPassiveMountEffects_begin(finishedWork, root);
setCurrentDebugFiberInDEV(null);
}
function commitPassiveMountEffects_begin(
subtreeRoot: Fiber,
root: FiberRoot,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;
if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
commitPassiveMountEffects_complete(subtreeRoot, root);
}
}
}
function commitPassiveMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & Passive) !== NoFlags) {
try {
commitPassiveMountOnFiber(root, fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
function commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// 执行useEffect create
commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
break;
}
}
}12.5.2 cleanup函数的调用
cleanup函数的调用时机有两种:
- 组件更新时:先执行旧的cleanup,再执行新的create
- 组件卸载时:执行cleanup
jsx
function Component({ id }) {
useEffect(() => {
console.log('Effect create', id);
return () => {
console.log('Effect cleanup', id);
};
}, [id]);
return <div>{id}</div>;
}
// 首次渲染 id=1
// 输出:Effect create 1
// 更新 id=2
// 输出:
// Effect cleanup 1 (先执行旧的cleanup)
// Effect create 2 (再执行新的create)
// 卸载
// 输出:Effect cleanup 2cleanup函数的执行顺序
jsx
function Parent() {
useEffect(() => {
console.log('Parent effect');
return () => console.log('Parent cleanup');
}, []);
return <Child />;
}
function Child() {
useEffect(() => {
console.log('Child effect');
return () => console.log('Child cleanup');
}, []);
return <div>Child</div>;
}
// 首次渲染:
// Child effect
// Parent effect
// 卸载:
// Child cleanup
// Parent cleanupcleanup函数按照深度优先的顺序执行,先执行子组件的cleanup,再执行父组件的cleanup。
为什么useEffect是异步的?
useEffect异步执行有几个重要原因:
- 不阻塞渲染:让浏览器尽快绘制,提高用户体验
- 批量执行:可以将多个Effect合并执行
- 避免阻塞交互:用户可以立即与页面交互
jsx
function SlowEffect() {
useEffect(() => {
// 耗时操作
for (let i = 0; i < 1000000000; i++) {}
console.log('Effect done');
}, []);
return <div>Content</div>;
}
// 如果useEffect是同步的:
// 1. render阶段
// 2. commit阶段
// 3. 执行useEffect(阻塞1秒)
// 4. 浏览器绘制
// 结果:用户看到白屏1秒
// useEffect是异步的:
// 1. render阶段
// 2. commit阶段
// 3. 浏览器绘制(用户立即看到内容)
// 4. 执行useEffect(不阻塞)
// 结果:用户立即看到内容useEffect的调度优先级
useEffect使用NormalSchedulerPriority调度,优先级低于用户交互:
javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
// 使用NormalSchedulerPriority调度
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}这意味着:
- 如果有高优先级更新(如用户输入),会先处理高优先级更新
- useEffect可能会被延迟执行
- 保证了用户交互的流畅性
示例:Effect的完整生命周期
jsx
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect: count =', count);
const timer = setInterval(() => {
console.log('Timer: count =', count);
}, 1000);
return () => {
console.log('Cleanup: count =', count);
clearInterval(timer);
};
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// 首次渲染(count=0):
// Effect: count = 0
// Timer: count = 0(每秒输出)
// 点击按钮(count=1):
// Cleanup: count = 0(先清理旧的Effect)
// Effect: count = 1(再执行新的Effect)
// Timer: count = 1(每秒输出)
// 卸载:
// Cleanup: count = 1本章小结
本章深入讲解了React的commit阶段,这是React渲染流程的最后一个关键阶段。让我们回顾一下关键要点:
commit阶段的三个子阶段
before mutation阶段:DOM变更前
- 调用
getSnapshotBeforeUpdate - 调度
useEffect(异步执行) - 准备DOM变更
- 调用
mutation阶段:执行DOM操作
- Placement:插入新节点
- Update:更新节点属性
- Deletion:删除节点
- 切换current树
layout阶段:DOM变更后
- 执行
useLayoutEffect(同步) - 更新ref
- 调用生命周期方法
- 执行
Effect的执行时机
| Effect类型 | 执行阶段 | 是否同步 | 适用场景 |
|---|---|---|---|
| useLayoutEffect | layout阶段 | 同步 | 读取DOM布局、同步更新DOM |
| useEffect | commit后异步 | 异步 | 数据获取、订阅、日志 |
| componentDidMount | layout阶段 | 同步 | 类组件初始化 |
| componentDidUpdate | layout阶段 | 同步 | 类组件更新 |
关键设计原则
- commit阶段不可中断:保证DOM操作的原子性
- Effect分离:同步Effect(useLayoutEffect)和异步Effect(useEffect)
- 优先级保证:commit阶段使用最高优先级
- 顺序保证:cleanup先于create执行
性能优化建议
- 优先使用useEffect:除非必须同步读取DOM
- 避免在useLayoutEffect中执行耗时操作:会阻塞渲染
- 正确清理Effect:避免内存泄漏
- 合理使用依赖数组:避免不必要的Effect执行
思考题
- 为什么current树的切换发生在mutation和layout之间?
- useLayoutEffect和useEffect的cleanup函数执行顺序有什么区别?
- 如果在useLayoutEffect中触发setState,会发生什么?
- 为什么删除节点时要先执行cleanup,再删除DOM?
在下一章中,我们将学习React的事件系统,理解合成事件的工作原理和事件优先级。