React Reconciliation
问题
什么是 React Reconciliation?它的工作原理是什么?和 Diff 算法有什么关系?
答案
Reconciliation(协调) 是 React 用于比较两棵虚拟 DOM 树差异、并高效更新真实 DOM 的算法。它是 React 性能优化的核心,通过 Diff 算法 将 复杂度降低到 。
Reconciliation 概述
| 概念 | 说明 |
|---|---|
| Reconciliation | 比较新旧虚拟 DOM,决定如何更新 |
| Diff 算法 | Reconciliation 的具体实现 |
| Fiber | React 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 条件:
props没变化(浅比较)context没变化state没变化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 的算法。
核心思想:
- 同层比较:不跨层级比较, →
- 类型判断:类型不同直接替换子树
- Key 标识:通过 key 识别列表项移动
// Reconciliation 决定如何更新
<div> <div>
<A /> → <A /> // 复用
<B /> <C /> // B→C 类型不同,替换
</div> </div>
Q2: Diff 算法的三个策略是什么?
答案:
| 策略 | 假设 | 做法 |
|---|---|---|
| Tree Diff | 跨层移动少 | 只比较同层节点 |
| Component Diff | 相同类型相似结构 | 类型不同直接替换 |
| Element Diff | key 标识元素 | 使用 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 到末尾
优化建议:将移动的节点放在列表末尾,减少移动操作。