React 渲染流程
问题
React 的渲染流程是怎样的?Render 阶段和 Commit 阶段分别做什么?
答案
React 渲染分为两个主要阶段:Render 阶段(协调)和 Commit 阶段(提交)。Render 阶段计算变化,可以被中断;Commit 阶段执行 DOM 操作,不可中断。
渲染流程全景
触发更新
更新触发方式
| 方式 | 示例 | 说明 |
|---|---|---|
| 首次渲染 | ReactDOM.createRoot().render() | 初始挂载 |
| setState | setCount(1) | 状态更新 |
| forceUpdate | this.forceUpdate() | 强制更新(类组件) |
| Context | Provider 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;
}
}
}
| 阶段 | 操作 |
|---|---|
| Layout | componentDidMount |
| Layout | componentDidUpdate |
| Layout | useLayoutEffect |
| 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 Mutation | DOM 操作前 | getSnapshotBeforeUpdate |
| Mutation | 执行 DOM | 插入、更新、删除 DOM |
| Layout | DOM 操作后 | componentDidMount、useLayoutEffect |
// 顺序
1. Before Mutation → getSnapshotBeforeUpdate
2. Mutation → 实际 DOM 操作
3. Layout → componentDidMount/useLayoutEffect
4. 浏览器绘制
5. useEffect(异步)
Q3: useEffect 和 useLayoutEffect 的区别?
答案:
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 浏览器绘制后 | 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'); // 不立即渲染
// 事件结束后,一次性渲染
}
原理:
- 多个 setState 创建多个 Update
- Update 加入同一个更新队列
- 调度一次 Render
- Render 时批量处理所有 Update