React Reconciliation算法
约 2120 字大约 7 分钟
reactreconciliationdiff
2025-07-25
概述
Reconciliation(协调)是 React 将虚拟 DOM 树的变化映射到真实 DOM 的过程。React 通过一套启发式算法将 O(n³) 的树 diff 复杂度降低到 O(n),在保证性能的同时尽可能减少 DOM 操作。理解这一算法对于编写高性能 React 应用至关重要。
Diff 算法的核心假设
React 的 Diff 算法基于两个启发式假设:
- 不同类型的元素会产生不同的树 — 当根元素类型变化时,React 会销毁旧树并完全重建新树
- 通过 key 标识在多次渲染中保持稳定的子元素 — key 帮助 React 识别列表中哪些元素发生了变化
1. Tree Level 比较
React 只比较同一层级的节点,不会跨层级移动节点。
// 跨层级移动不会被优化,而是销毁+重建
// Before
<div>
<A>
<B />
<C />
</A>
</div>
// After — 即使只是将 A 移到了另一个位置
<div>
<D>
<A>
<B />
<C />
</A>
</D>
</div>
// React 不会"移动" A,而是销毁旧 A 并在 D 下创建新 A2. Component Level 比较
同类型组件
组件类型不变时,React 保留组件实例和 DOM,只更新 props。
// 同类型组件:保留实例,更新 props
<UserProfile name="Alice" />
// ↓ 更新
<UserProfile name="Bob" />
// React 调用 componentDidUpdate / useEffect
// 组件内部状态保留
// 类型变化:销毁旧组件,创建新组件
<UserProfile name="Alice" />
// ↓ 更新
<AdminProfile name="Alice" />
// 即使 props 相同,组件完全重建,状态丢失强制重置组件状态
// 使用 key 强制重建组件(重置内部状态)
function ChatRoom({ roomId }) {
// 当 roomId 变化时,希望重置聊天状态
return <Chat key={roomId} roomId={roomId} />;
// key 变化 → React 销毁旧 Chat,创建新 Chat
}3. Element Level 比较
DOM 元素属性 Diff
// 只更新变化的属性
<div className="old" title="hello" />
// ↓
<div className="new" title="hello" />
// React 只修改 className,title 不变
// style 属性的精细 diff
<div style={{ color: 'red', fontSize: 14 }} />
// ↓
<div style={{ color: 'blue', fontSize: 14 }} />
// React 只修改 color,fontSize 不变子节点 Diff(无 key)
无 key 时按索引逐个比较:
// Before
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
// After
<ul>
<li>Cherry</li>
<li>Apple</li>
<li>Banana</li>
</ul>
// 无 key:按索引比较
// li[0]: Apple → Cherry (更新文本)
// li[1]: Banana → Apple (更新文本)
// li[2]: null → Banana (新增)
// 效率低:3 次 DOM 操作4. Key 的作用与优化
有 key 的列表 Diff
// Before
<ul>
<li key="a">Apple</li>
<li key="b">Banana</li>
</ul>
// After
<ul>
<li key="c">Cherry</li>
<li key="a">Apple</li>
<li key="b">Banana</li>
</ul>
// 有 key:按 key 匹配
// key="a": 移动位置(复用 DOM)
// key="b": 移动位置(复用 DOM)
// key="c": 新增
// 效率高:1 次插入操作Key 的最佳实践
// Good: 使用稳定的唯一标识
{items.map(item => (
<ListItem key={item.id} data={item} />
))}
// Bad: 使用索引作为 key(在列表变化时导致问题)
{items.map((item, index) => (
<ListItem key={index} data={item} />
// 在头部插入元素时,所有索引偏移,导致全量更新
))}
// Bad: 使用随机值(每次渲染生成新 key,导致全量重建)
{items.map(item => (
<ListItem key={Math.random()} data={item} />
))}index 作为 key 的具体问题
function App() {
const [items, setItems] = useState([
{ id: 1, text: 'First' },
{ id: 2, text: 'Second' },
{ id: 3, text: 'Third' }
]);
const addToFront = () => {
setItems([{ id: 4, text: 'New' }, ...items]);
};
return (
<div>
<button onClick={addToFront}>Add to Front</button>
{items.map((item, index) => (
// key={index} 时:
// Before: key=0(First), key=1(Second), key=2(Third)
// After: key=0(New), key=1(First), key=2(Second), key=3(Third)
// React 认为 key=0 的元素内容从 First 变为 New,全部更新
<input key={index} defaultValue={item.text} />
))}
</div>
);
}5. Fiber 中的 Reconciliation 细节
beginWork 阶段
function beginWork(current, workInProgress) {
// current: 当前屏幕上的 Fiber
// workInProgress: 正在构建的 Fiber
if (current !== null) {
// 更新路径
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasContextChanged()) {
// props 变化,需要重新渲染
didReceiveUpdate = true;
} else {
// 可能走 bailout 路径
didReceiveUpdate = false;
}
} else {
// 首次挂载
didReceiveUpdate = false;
}
// 根据组件类型执行不同的协调逻辑
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress);
case ClassComponent:
return updateClassComponent(current, workInProgress);
case HostComponent:
return updateHostComponent(current, workInProgress);
// ...
}
}reconcileChildFibers(子节点协调)
// 简化的子节点协调逻辑
function reconcileChildFibers(returnFiber, currentChild, newChild) {
// 1. 单个元素
if (typeof newChild === 'object' && newChild !== null) {
if (newChild.$$typeof === REACT_ELEMENT_TYPE) {
return reconcileSingleElement(returnFiber, currentChild, newChild);
}
}
// 2. 文本节点
if (typeof newChild === 'string' || typeof newChild === 'number') {
return reconcileSingleTextNode(returnFiber, currentChild, newChild);
}
// 3. 数组(列表)
if (Array.isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentChild, newChild);
}
// 4. 删除所有旧子节点
return deleteRemainingChildren(returnFiber, currentChild);
}6. 列表 Diff 详细算法
React 的列表 Diff 分为两轮遍历:
// 简化的列表 diff 算法
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
let oldFiber = currentFirstChild;
let newIdx = 0;
let lastPlacedIndex = 0;
// 第一轮:按顺序比较
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
if (oldFiber.key !== newChildren[newIdx].key) break;
// key 相同,复用
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
oldFiber = oldFiber.sibling;
}
// 如果新列表已遍历完,删除剩余旧节点
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return;
}
// 如果旧列表已遍历完,创建剩余新节点
if (!oldFiber) {
for (; newIdx < newChildren.length; newIdx++) {
createChild(returnFiber, newChildren[newIdx]);
}
return;
}
// 第二轮:使用 Map 处理移动/插入/删除
const existingChildren = mapRemainingChildren(oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(existingChildren, newChildren[newIdx]);
if (newFiber) {
existingChildren.delete(newFiber.key || newIdx);
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
}
}
// 删除未匹配的旧节点
existingChildren.forEach(child => deleteChild(returnFiber, child));
}7. Bailout 优化
当 React 检测到组件无需更新时,可以跳过整棵子树的协调。
// React.memo: 浅比较 props
const MemoizedList = React.memo(function List({ items }) {
return items.map(item => <Item key={item.id} data={item} />);
});
// 只有 items 引用变化时才重渲染
// useMemo: 缓存计算结果
function App({ data }) {
const processed = useMemo(() => {
return expensiveProcess(data);
}, [data]);
return <Display data={processed} />;
}
// useCallback: 缓存函数引用
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 引用稳定
return <MemoizedChild onClick={handleClick} />;
}Bailout 条件
8. 性能优化建议
// 1. 保持组件类型稳定
// Bad: 条件渲染不同类型会导致状态丢失
function Bad() {
return isLoggedIn ? <AdminDashboard /> : <UserDashboard />;
// 切换时完全销毁重建
}
// 2. 将变化隔离到最小组件
// Bad: 父组件状态变化导致所有子组件重渲染
function Bad() {
const [color, setColor] = useState('red');
return (
<div style={{ color }}>
<ExpensiveTree /> {/* 不需要 color,但每次都重渲染 */}
</div>
);
}
// Good: 将状态下移到需要的组件
function Good() {
return (
<div>
<ColorPicker /> {/* 状态在内部管理 */}
<ExpensiveTree /> {/* 不会被 color 变化影响 */}
</div>
);
}
// 3. 提升不变的子树
function Good({ color }) {
return (
<div style={{ color }}>
{children} {/* children 引用不变 → bailout */}
</div>
);
}总结
React Reconciliation 算法通过三个层级的比较策略——Tree Level(同层比较)、Component Level(类型比较)、Element Level(属性和 key 比较)——将树 diff 的复杂度从 O(n³) 降低到 O(n)。key 是列表渲染性能的关键,使用稳定唯一的标识符能让 React 准确识别节点的增删移动。理解 bailout 机制并合理使用 React.memo、useMemo、useCallback,可以显著减少不必要的协调工作。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于