跳到主要内容

React Reconciliation

问题

什么是 React Reconciliation?它的工作原理是什么?和 Diff 算法有什么关系?

答案

Reconciliation(协调) 是 React 用于比较两棵虚拟 DOM 树差异、并高效更新真实 DOM 的算法。它是 React 性能优化的核心,通过 Diff 算法O(n3)O(n^3) 复杂度降低到 O(n)O(n)

Reconciliation 概述

概念说明
Reconciliation比较新旧虚拟 DOM,决定如何更新
Diff 算法Reconciliation 的具体实现
FiberReact 16+ 的 Reconciliation 架构

Diff 算法的三个策略

策略一:Tree Diff

假设:跨层级移动 DOM 节点极少

策略:只比较同层级节点,不跨层级比较

// 跨层级移动会被视为"删除+创建"
// 旧树
<div>
<A>
<B />
</A>
</div>

// 新树 - B 移动到 A 外部
<div>
<A />
<B /> {/* React: 删除旧 B,创建新 B */}
</div>
性能提示

跨层级移动 DOM 会导致整个子树重新创建。在 React 中应尽量避免这种操作模式。

策略二:Component Diff

假设:相同类型的组件生成相似的树结构

策略

  • 类型相同:继续比较子节点
  • 类型不同:直接替换整个子树
// 类型相同 - 更新属性
<Button color="red" /><Button color="blue" />
// 复用组件实例,只更新 props

// 类型不同 - 整个替换
<Button /><Link />
// 销毁 Button,创建 Link

策略三:Element Diff

假设:同一层级的子元素可以通过 key 标识

策略:通过 key 识别元素,实现移动而非重建

// 没有 key - 按位置比较
['A', 'B', 'C']['C', 'A', 'B']
// A→C(更新),B→A(更新),C→B(更新)
// 3 次更新

// 有 key - 按 key 匹配
[{key:'a'}, {key:'b'}, {key:'c'}][{key:'c'}, {key:'a'}, {key:'b'}]
// 识别为移动操作
// 只需移动 DOM 节点

Fiber Reconciliation

双缓冲机制

React 维护两棵 Fiber 树:

// 简化的双缓冲原理
let current: Fiber | null = null;
let workInProgress: Fiber | null = null;

function commitRoot() {
// 完成时交换指针
current = workInProgress;
workInProgress = null;
}

Reconciliation 两个阶段

1. Render 阶段(可中断)

// beginWork: 处理当前节点
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// 比较更新前后
if (current !== null) {
// 更新:复用或创建新 Fiber
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

if (oldProps !== newProps) {
// 需要更新
didReceiveUpdate = true;
}
}

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

// completeWork: 完成当前节点
function completeWork(
current: Fiber | null,
workInProgress: Fiber
): Fiber | null {
// 收集副作用
// 生成更新标记
if (current !== null && workInProgress.stateNode != null) {
// 更新
markUpdate(workInProgress);
} else {
// 创建
workInProgress.stateNode = createInstance(workInProgress);
}

return null;
}

2. Commit 阶段(不可中断)

function commitRoot(root: FiberRoot) {
// Before Mutation 阶段
commitBeforeMutationEffects(root);

// Mutation 阶段 - DOM 操作
commitMutationEffects(root);

// 切换 current 树
root.current = finishedWork;

// Layout 阶段
commitLayoutEffects(root);
}

单节点 Diff

当新子节点是单个元素时:

function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement
): Fiber {
const key = element.key;
let child = currentFirstChild;

while (child !== null) {
if (child.key === key) {
// key 相同
if (child.elementType === element.type) {
// 类型相同:复用
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
return existing;
} else {
// 类型不同:删除所有旧节点
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
// key 不同:删除这个旧节点
deleteChild(returnFiber, child);
}
child = child.sibling;
}

// 创建新节点
const created = createFiberFromElement(element);
return created;
}

多节点 Diff

当新子节点是数组时,React 会进行两轮遍历

第一轮:处理更新

// 第一轮遍历
let i = 0;
let lastPlacedIndex = 0;
let newIdx = 0;
const newChildren = Array.isArray(newChild) ? newChild : [newChild];
let oldFiber = currentFirstChild;

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
const newChild = newChildren[newIdx];

if (oldFiber.key !== newChild.key) {
// key 不同,跳出第一轮遍历
break;
}

if (oldFiber.type === newChild.type) {
// 复用
newFiber = useFiber(oldFiber, newChild.props);
} else {
// 类型不同,创建新的
newFiber = createFiber(newChild);
}

oldFiber = oldFiber.sibling;
}

第二轮:处理新增、删除、移动

// 情况1:新节点遍历完 - 删除剩余旧节点
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return;
}

// 情况2:旧节点遍历完 - 新增剩余新节点
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createFiber(newChildren[newIdx]);
placeChild(newFiber, lastPlacedIndex, newIdx);
}
return;
}

// 情况3:都未遍历完 - 处理移动
const existingChildren = mapRemainingChildren(oldFiber);

for (; newIdx < newChildren.length; newIdx++) {
const newChild = newChildren[newIdx];
const matchedFiber = existingChildren.get(newChild.key);

if (matchedFiber) {
// 找到可复用的节点
const newFiber = useFiber(matchedFiber, newChild.props);
const oldIndex = matchedFiber.index;

if (oldIndex < lastPlacedIndex) {
// 需要移动
newFiber.flags |= Placement;
} else {
// 不需要移动
lastPlacedIndex = oldIndex;
}

existingChildren.delete(newChild.key);
} else {
// 新建节点
const newFiber = createFiber(newChild);
newFiber.flags |= Placement;
}
}

// 删除未匹配的旧节点
existingChildren.forEach(child => {
deleteChild(returnFiber, child);
});

移动判断:lastPlacedIndex

// 旧: A(0) B(1) C(2) D(3)
// 新: A C B D

// 遍历新列表:
// A: oldIndex=0, lastPlacedIndex=0, 不移动, lastPlacedIndex=0
// C: oldIndex=2, lastPlacedIndex=0, 不移动, lastPlacedIndex=2
// B: oldIndex=1, lastPlacedIndex=2, 1<2 需要移动
// D: oldIndex=3, lastPlacedIndex=2, 不移动, lastPlacedIndex=3

// 结果:只需要移动 B

Reconciliation 优化

bailout 优化

当满足条件时,跳过 Reconciliation:

function bailoutOnAlreadyFinishedWork(
current: Fiber,
workInProgress: Fiber
): Fiber | null {
// 检查子树是否有更新
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// 子树没有更新,跳过整个子树
return null;
}

// 子树有更新,复用当前节点,继续处理子节点
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}

bailout 条件:

  1. props 没变化(浅比较)
  2. context 没变化
  3. state 没变化
  4. type 没变化

eagerState 优化

在 Reconciliation 前计算新状态:

function dispatchSetState<S>(
fiber: Fiber,
queue: UpdateQueue<S>,
action: S | ((prevState: S) => S)
) {
// 计算新状态
const eagerState = typeof action === 'function'
? (action as (s: S) => S)(currentState)
: action;

// 如果状态没变,可以提前退出
if (Object.is(eagerState, currentState)) {
return; // 不触发 Reconciliation
}

// 继续调度更新
scheduleUpdateOnFiber(fiber);
}

常见面试问题

Q1: 什么是 Reconciliation?

答案

Reconciliation(协调)是 React 比较新旧虚拟 DOM 树差异,并高效更新真实 DOM 的算法。

核心思想:

  1. 同层比较:不跨层级比较,O(n3)O(n^3)O(n)O(n)
  2. 类型判断:类型不同直接替换子树
  3. Key 标识:通过 key 识别列表项移动
// Reconciliation 决定如何更新
<div> <div>
<A /><A /> // 复用
<B /> <C /> // B→C 类型不同,替换
</div> </div>

Q2: Diff 算法的三个策略是什么?

答案

策略假设做法
Tree Diff跨层移动少只比较同层节点
Component Diff相同类型相似结构类型不同直接替换
Element Diffkey 标识元素使用 key 识别移动
// Tree Diff - 同层比较
<div> <div>
<A /><B /> // 只比较这一层
</div> </div>

// Component Diff - 类型判断
<Button /><Link /> // 类型不同,整个替换

// Element Diff - key 标识
[A, B, C][C, A, B] // 通过 key 识别为移动

Q3: 为什么需要 key?key 的作用是什么?

答案

作用:帮助 React 在 Reconciliation 中识别列表项身份

没有 key 的问题

// 旧: [A, B, C]
// 新: [D, A, B, C]

// 没有 key:按位置比较
// A→D, B→A, C→B, 新增C(4次操作)

// 有 key:识别为头部插入
// 插入 D(1次操作)

key 的要求

  • 稳定:不能用 Math.random()
  • 唯一:同级元素 key 不重复
  • 不推荐用 index:删除/插入时会出问题

Q4: Reconciliation 的两个阶段是什么?

答案

阶段特点工作
Render 阶段⏸️ 可中断构建 workInProgress 树,标记副作用
Commit 阶段▶️ 不可中断执行 DOM 操作,调用生命周期

为什么 Commit 不可中断

  • DOM 操作必须同步完成
  • 避免用户看到中间状态

Q5: 多节点 Diff 的 lastPlacedIndex 是什么?

答案

lastPlacedIndex 用于判断节点是否需要移动

规则

  • 记录最后一个不需要移动的旧节点索引
  • 如果 oldIndex < lastPlacedIndex,需要移动
// 旧: A(0) B(1) C(2) D(3)
// 新: A C D B

// 遍历新列表:
// A: oldIdx=0 >= lastPlacedIdx=0 ✅ 不移动, lastPlacedIdx=0
// C: oldIdx=2 >= lastPlacedIdx=0 ✅ 不移动, lastPlacedIdx=2
// D: oldIdx=3 >= lastPlacedIdx=2 ✅ 不移动, lastPlacedIdx=3
// B: oldIdx=1 < lastPlacedIdx=3 ❌ 需要移动

// 结果:只移动 B 到末尾

优化建议:将移动的节点放在列表末尾,减少移动操作。

相关链接