跳到主要内容

React 渲染流程

问题

React 的渲染流程是怎样的?Render 阶段和 Commit 阶段分别做什么?

答案

React 渲染分为两个主要阶段:Render 阶段(协调)和 Commit 阶段(提交)。Render 阶段计算变化,可以被中断;Commit 阶段执行 DOM 操作,不可中断。

渲染流程全景

触发更新

更新触发方式

方式示例说明
首次渲染ReactDOM.createRoot().render()初始挂载
setStatesetCount(1)状态更新
forceUpdatethis.forceUpdate()强制更新(类组件)
ContextProvider value 变化Context 更新
// 所有更新都会创建 Update 对象并调度
function dispatchSetState<S>(
fiber: Fiber,
queue: UpdateQueue<S>,
action: S
) {
// 1. 创建更新对象
const update: Update<S> = {
lane: requestUpdateLane(),
action,
next: null,
};

// 2. 将更新加入队列
enqueueUpdate(fiber, update);

// 3. 调度更新
scheduleUpdateOnFiber(fiber);
}

调度阶段

React 使用 Scheduler 调度任务,基于优先级系统:

// 调度更新
function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
// 1. 向上找到根节点
const root = markUpdateLaneFromFiberToRoot(fiber, lane);

// 2. 标记根节点有更新
markRootUpdated(root, lane);

// 3. 调度任务
ensureRootIsScheduled(root);
}

function ensureRootIsScheduled(root: FiberRoot) {
// 根据优先级调度
const nextLanes = getNextLanes(root);
const priority = lanesToSchedulerPriority(nextLanes);

// 调度 Concurrent 任务
scheduleCallback(priority, performConcurrentWorkOnRoot.bind(null, root));
}

优先级系统(Lanes)

// React 18 的 Lane 优先级
const SyncLane = 0b0000000000000000000000000000001; // 同步
const InputContinuousLane = 0b0000000000000000000000000000100; // 连续输入
const DefaultLane = 0b0000000000000000000000000000010000; // 默认
const TransitionLane = 0b0000000000000000000001000000000000; // 过渡
const IdleLane = 0b0100000000000000000000000000000000; // 空闲

Render 阶段

工作循环

function workLoopConcurrent() {
// 可中断的工作循环
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}

function workLoopSync() {
// 同步的工作循环(不可中断)
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

performUnitOfWork

function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;

// 1. beginWork: 处理当前节点
let next = beginWork(current, unitOfWork, renderLanes);

unitOfWork.memoizedProps = unitOfWork.pendingProps;

if (next === null) {
// 2. 没有子节点,完成当前节点
completeUnitOfWork(unitOfWork);
} else {
// 3. 有子节点,继续处理子节点
workInProgress = next;
}
}

beginWork

处理当前 Fiber 节点,返回子节点:

function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {

// 优化:检查是否可以跳过
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

if (oldProps === newProps && !hasContextChanged()) {
// props 没变,可能可以跳过
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
}

// 根据组件类型处理
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, renderLanes);

case ClassComponent:
return updateClassComponent(current, workInProgress, renderLanes);

case HostComponent: // div, span 等
return updateHostComponent(current, workInProgress, renderLanes);

case HostText: // 文本节点
return null; // 文本没有子节点

// ... 其他类型
}
}

函数组件处理

function updateFunctionComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// 1. 执行函数组件,获取子元素
const children = renderWithHooks(
current,
workInProgress,
Component,
props,
renderLanes
);

// 2. 协调子节点
reconcileChildren(current, workInProgress, children, renderLanes);

return workInProgress.child;
}

function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: Function,
props: any,
renderLanes: Lanes
) {
// 设置当前 Fiber
currentlyRenderingFiber = workInProgress;

// 重置 hooks 链表
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;

// 选择 hooks 实现
ReactCurrentDispatcher.current = current === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

// 执行组件函数
const children = Component(props);

return children;
}

completeWork

完成当前节点,处理 DOM 和副作用:

function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {

switch (workInProgress.tag) {
case HostComponent: {
if (current !== null && workInProgress.stateNode != null) {
// 更新:对比属性变化
updateHostComponent(current, workInProgress);
} else {
// 创建:生成 DOM 节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
);

// 将子节点 DOM 插入到当前 DOM
appendAllChildren(instance, workInProgress);

workInProgress.stateNode = instance;
}

// 冒泡副作用标记
bubbleProperties(workInProgress);
return null;
}

// ... 其他类型
}
}

Fiber 遍历顺序

Commit 阶段

三个子阶段

Before Mutation 阶段

function commitBeforeMutationEffects(root: FiberRoot, firstChild: Fiber) {
nextEffect = firstChild;

while (nextEffect !== null) {
const fiber = nextEffect;

// 类组件:调用 getSnapshotBeforeUpdate
if ((fiber.flags & Snapshot) !== NoFlags) {
const current = fiber.alternate;
commitBeforeMutationEffectOnFiber(current, fiber);
}

nextEffect = fiber.nextEffect;
}
}
操作说明
getSnapshotBeforeUpdate类组件获取更新前的 DOM 快照
调度 useEffect异步调度(不在这执行)

Mutation 阶段

执行真正的 DOM 操作:

function commitMutationEffects(root: FiberRoot, firstChild: Fiber) {
while (nextEffect !== null) {
const fiber = nextEffect;
const flags = fiber.flags;

// 1. 处理 ref 重置
if (flags & Ref) {
commitAttachRef(fiber);
}

// 2. 处理 DOM 操作
const primaryFlags = flags & (Placement | Update | Deletion);

switch (primaryFlags) {
case Placement:
// 插入
commitPlacement(fiber);
fiber.flags &= ~Placement;
break;

case Update:
// 更新
commitWork(fiber);
break;

case Deletion:
// 删除
commitDeletion(root, fiber);
break;

case PlacementAndUpdate:
// 移动并更新
commitPlacement(fiber);
fiber.flags &= ~Placement;
commitWork(fiber);
break;
}

nextEffect = fiber.nextEffect;
}
}
// 插入 DOM
function commitPlacement(finishedWork: Fiber) {
const parentFiber = getHostParentFiber(finishedWork);
const parent = parentFiber.stateNode;

const before = getHostSibling(finishedWork);

if (before) {
insertBefore(parent, finishedWork.stateNode, before);
} else {
appendChild(parent, finishedWork.stateNode);
}
}

// 更新 DOM 属性
function commitWork(finishedWork: Fiber) {
switch (finishedWork.tag) {
case HostComponent: {
const instance = finishedWork.stateNode;
const updatePayload = finishedWork.updateQueue;

// 更新属性
updateDOMProperties(instance, updatePayload);
break;
}
// ...
}
}

Layout 阶段

DOM 操作完成后执行:

function commitLayoutEffects(root: FiberRoot, firstChild: Fiber) {
while (nextEffect !== null) {
const fiber = nextEffect;
const flags = fiber.flags;

// 1. 调用生命周期/hooks
if (flags & (Update | Callback)) {
commitLayoutEffectOnFiber(root, fiber.alternate, fiber);
}

// 2. 绑定 ref
if (flags & Ref) {
commitAttachRef(fiber);
}

nextEffect = fiber.nextEffect;
}
}

function commitLayoutEffectOnFiber(
root: FiberRoot,
current: Fiber | null,
finishedWork: Fiber
) {
switch (finishedWork.tag) {
case FunctionComponent: {
// 执行 useLayoutEffect
commitHookEffectListMount(HookLayout, finishedWork);
break;
}

case ClassComponent: {
const instance = finishedWork.stateNode;
if (current === null) {
// 首次渲染
instance.componentDidMount();
} else {
// 更新
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
);
}
break;
}
}
}
阶段操作
LayoutcomponentDidMount
LayoutcomponentDidUpdate
LayoutuseLayoutEffect
Layout 后异步useEffect

useEffect vs useLayoutEffect

function Example() {
useLayoutEffect(() => {
// 同步执行,阻塞浏览器绘制
// 适合:需要同步读取/修改 DOM
console.log('useLayoutEffect');
});

useEffect(() => {
// 异步执行,不阻塞绘制
// 适合:数据请求、订阅等
console.log('useEffect');
});

return <div>Example</div>;
}

// 输出顺序:
// useLayoutEffect
// (浏览器绘制)
// useEffect

完整渲染流程示例

function App() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
</div>
);
}

常见面试问题

Q1: React 渲染分为哪两个阶段?各自的特点是什么?

答案

阶段特点工作
Render 阶段可中断计算变化,构建 Fiber 树
Commit 阶段不可中断执行 DOM 操作

为什么 Commit 不可中断

  • 用户需要看到一致的 UI
  • DOM 操作必须原子完成

Q2: Commit 阶段的三个子阶段分别做什么?

答案

子阶段时机操作
Before MutationDOM 操作前getSnapshotBeforeUpdate
Mutation执行 DOM插入、更新、删除 DOM
LayoutDOM 操作后componentDidMount、useLayoutEffect
// 顺序
1. Before Mutation → getSnapshotBeforeUpdate
2. Mutation → 实际 DOM 操作
3. Layout → componentDidMount/useLayoutEffect
4. 浏览器绘制
5. useEffect(异步)

Q3: useEffect 和 useLayoutEffect 的区别?

答案

特性useEffectuseLayoutEffect
执行时机浏览器绘制后DOM 更新后、绘制前
是否阻塞不阻塞阻塞绘制
适用场景数据请求、订阅同步读取/修改 DOM
useLayoutEffect(() => {
// 同步执行,可以读取布局信息
const rect = element.getBoundingClientRect();
// 可以在绘制前修改 DOM,避免闪烁
element.style.left = `${rect.x}px`;
}, []);

useEffect(() => {
// 异步执行,适合副作用
fetchData();
}, []);

Q4: beginWork 和 completeWork 分别做什么?

答案

beginWork(递):

  • 处理当前 Fiber 节点
  • 执行组件函数/render
  • 创建子 Fiber 节点
  • 返回子 Fiber

completeWork(归):

  • 创建/更新 DOM 节点
  • 收集副作用标记
  • 冒泡 flags 给父节点
// 遍历顺序
App (beginWork)
Header (beginWork)
Header (completeWork)
Main (beginWork)
Article (beginWork)
Article (completeWork)
Main (completeWork)
App (completeWork)

Q5: React 是如何进行批量更新的?

答案

React 18 使用自动批处理,在调度层面合并更新:

function handleClick() {
setCount(c => c + 1); // 不立即渲染
setFlag(true); // 不立即渲染
setName('Alice'); // 不立即渲染
// 事件结束后,一次性渲染
}

原理

  1. 多个 setState 创建多个 Update
  2. Update 加入同一个更新队列
  3. 调度一次 Render
  4. Render 时批量处理所有 Update

相关链接