说说对 React 的 diff 算法的理解?
问题解析(面试官考察点)
面试官通过此问题主要考察:
- 对 React Diff 算法核心思想的理解
- 对 Virtual DOM 与真实 DOM 对比机制的了解
- 对 Diff 算法三个层级策略的掌握
- 对 React 性能优化原理的认识
核心概念(基础知识点)
什么是 Diff 算法
Diff 算法是 React 用于比较两棵 Virtual DOM 树(新旧树)差异的算法。它的核心目标是以最小的代价找出需要更新的部分,从而最小化对真实 DOM 的操作。
Diff 算法的核心思想
- O(n) 复杂度: 将对比复杂度从 O(n³) 降低到 O(n)
- 分层比较: 只比较同层节点,不跨层比较
- 类型比较: 不同类型的元素直接替换
- Key 优化: 使用 key 优化列表对比
详细解答(代码示例)
Diff 算法的三个层级
┌─────────────────────────────────────────────────────────────┐
│ React Diff 三个层级 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Tree Diff(树层级) │
│ - 逐层对比 │
│ - 跨层节点直接删除/创建 │
│ │
│ 2. Component Diff(组件层级) │
│ - 不同类型组件直接替换 │
│ - 同类型组件对比 props 和 state │
│ │
│ 3. Element Diff(元素层级) │
│ - 使用 key 优化列表对比 │
│ - 三种操作:插入、移动、删除 │
│ │
└─────────────────────────────────────────────────────────────┘
Tree Diff(树层级对比)
// Tree Diff 策略:逐层对比,跨层操作直接删除/创建
// 旧树
const oldTree = (
<div>
<p>Hello</p>
<span>World</span>
</div>
);
// 新树 - 跨层移动(不推荐,性能差)
const newTree = (
<div>
<section>
<p>Hello</p>
</section>
<span>World</span>
</div>
);
// React 的处理:
// 1. 发现 div 的子元素从 [p, span] 变为 [section, span]
// 2. p 元素被移动到 section 内部(跨层)
// 3. React 不会尝试移动,而是:
// - 删除原来的 p
// - 创建新的 section
// - 在 section 中创建新的 p
// 4. span 保持不变
// 性能建议:避免跨层移动节点
Component Diff(组件层级对比)
// Component Diff 策略:不同类型直接替换
// 场景1:相同类型的组件
class Counter extends React.Component {
render() {
return <div>{this.props.count}</div>;
}
}
// 旧:<Counter count={0} />
// 新:<Counter count={1} />
// React 会保留组件实例,只更新 props
// 场景2:不同类型的组件
// 旧:<Counter count={0} />
// 新:<div>0</div>
// React 会卸载 Counter 组件,创建新的 div 元素
// 场景3:自定义组件 vs 原生元素
function Demo() {
const [useComponent, setUseComponent] = useState(true);
return (
<div>
{useComponent ? (
<MyComponent /> // 自定义组件
) : (
<div>原生元素</div> // 原生元素
)}
</div>
);
}
// 切换时,MyComponent 会被完全卸载,div 会被重新创建
Element Diff(元素层级对比)
// Element Diff 策略:使用 key 优化列表对比
// 场景1:列表末尾插入(最优情况)
// 旧:[A, B, C]
// 新:[A, B, C, D]
// React:直接在末尾插入 D
// 场景2:列表开头插入(使用 key 优化)
// 旧:[A(key=a), B(key=b), C(key=c)]
// 新:[D(key=d), A(key=a), B(key=b), C(key=c)]
// React:识别出 A、B、C 只是后移,插入 D 到开头
// 场景3:没有 key 的问题
// 旧:[A, B, C](使用索引作为 key:0, 1, 2)
// 新:[D, A, B, C](使用索引作为 key:0, 1, 2, 3)
// React:认为所有元素都变了,全部更新(性能差)
// 正确的列表渲染
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Diff 算法源码简化
// React Diff 算法简化示意
// 1. 单节点对比
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
const key = element.key;
const child = currentFirstChild;
// 尝试找到匹配的子节点
while (child !== null) {
if (child.key === key) {
if (child.type === element.type) {
// 找到匹配的节点,复用
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
} else {
// key 相同但类型不同,删除旧节点
deleteChild(returnFiber, child);
}
} else {
// key 不同,删除旧节点
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 创建新节点
const created = createFiberFromElement(element, returnFiber.mode);
created.return = returnFiber;
return created;
}
// 2. 多节点对比(列表)
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
let resultingFirstChild = null;
let previousNewFiber = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
// 第一轮:按位置对比
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (newFiber === null) {
// key 不同,跳出循环
break;
}
if (oldFiber && newFiber.alternate === null) {
// 删除旧节点
deleteChild(returnFiber, oldFiber);
}
// 记录放置位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 第二轮:处理剩余的新元素或删除旧元素
if (newIdx === newChildren.length) {
// 新元素处理完毕,删除剩余旧元素
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 第三轮:处理移动(基于 key 的 map)
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx]
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 复用的节点,从 map 中移除
existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
}
}
// 判断是否需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 删除未复用的旧节点
if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
深入理解(原理剖析)
Diff 算法的复杂度优化
┌─────────────────────────────────────────────────────────────┐
│ Diff 算法复杂度对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 传统算法(树编辑距离) │
│ - 需要找出两棵树之间的最小转换操作 │
│ - 时间复杂度:O(n³) │
│ - 1000 个节点需要 10 亿次比较 │
│ │
│ React Diff 算法 │
│ - 基于三个策略的启发式算法 │
│ - 时间复杂度:O(n) │
│ - 1000 个节点只需要 1000 次比较 │
│ │
│ 优化策略: │
│ 1. Web UI 中跨层移动节点很少 │
│ 2. 不同类型的组件会产生不同的树结构 │
│ 3. 可以通过 key 提示哪些子元素是稳定的 │
│ │
└─────────────────────────────────────────────────────────────┘
Diff 算法的执行流程
┌─────────────────────────────────────────────────────────────┐
│ Diff 算法执行流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 开始对比 │
│ currentFiber vs workInProgress │
│ │
│ 2. 判断节点类型 │
│ ├─ 相同类型 → 复用节点(更新 props) │
│ └─ 不同类型 → 删除旧节点,创建新节点 │
│ │
│ 3. 处理子节点 │
│ ├─ 单个子节点 → 直接对比 │
│ ├─ 多个子节点(列表)→ 使用 key 优化对比 │
│ └─ 文本节点 → 对比文本内容 │
│ │
│ 4. 标记副作用 │
│ ├─ Placement(插入) │
│ ├─ Update(更新) │
│ ├─ Deletion(删除) │
│ └─ PlacementAndUpdate(移动并更新) │
│ │
│ 5. 继续遍历子树 │
│ │
└─────────────────────────────────────────────────────────────┘
双缓冲技术(Double Buffering)
// React 使用双缓冲技术优化渲染
// current Fiber Tree(当前显示在屏幕上的树)
const current = {
type: 'div',
props: { children: 'Hello' },
stateNode: domNode, // 对应的真实 DOM
alternate: workInProgress // 指向 workInProgress
};
// workInProgress Fiber Tree(正在构建的树)
const workInProgress = {
type: 'div',
props: { children: 'World' },
stateNode: null, // 还未创建真实 DOM
alternate: current // 指向 current
};
// Diff 过程:
// 1. 基于 current 创建 workInProgress
// 2. 在 workInProgress 上进行更新
// 3. 更新完成后,交换两者:current = workInProgress
// 4. workInProgress 变为下一次更新的 current
最佳实践
1. 使用 Key 优化列表
// 推荐:使用稳定的唯一标识作为 key
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 避免:使用索引作为 key(列表变化时性能差)
function BadUserList({ users }) {
return (
<ul>
{users.map((user, index) => (
<li key={index}>{user.name}</li>
))}
</ul>
);
}
2. 避免跨层移动节点
// 不好的做法:条件渲染改变层级
function BadExample({ showDetail }) {
return (
<div>
{showDetail ? (
<DetailWrapper>
<Header />
<Content />
</DetailWrapper>
) : (
<>
<Header />
<Content />
</>
)}
</div>
);
}
// 切换时 Header 和 Content 会被销毁重建
// 好的做法:保持层级稳定
function GoodExample({ showDetail }) {
return (
<div>
<Header />
<Content />
{showDetail && <Detail />}
</div>
);
}
3. 组件类型保持一致
// 不好的做法:切换组件类型
function BadExample({ useList }) {
return (
<div>
{useList ? (
<ListView items={items} />
) : (
<GridView items={items} />
)}
</div>
);
}
// 好的做法:使用相同的包装组件
function GoodExample({ useList }) {
return (
<div>
<ViewContainer layout={useList ? 'list' : 'grid'}>
{items.map(item => <Item key={item.id} {...item} />)}
</ViewContainer>
</div>
);
}
4. 使用 React.memo 减少不必要的 Diff
// 使用 React.memo 避免不必要的对比
const ExpensiveComponent = React.memo(({ data }) => {
// 复杂的渲染逻辑
return <div>{/* ... */}</div>;
});
// 自定义比较函数
const CustomComponent = React.memo(
({ user }) => {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// 只比较 name,忽略其他属性变化
return prevProps.user.name === nextProps.user.name;
}
);
5. 虚拟列表优化长列表
// 使用 react-window 优化长列表
import { FixedSizeList as List } from "react-window";
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<List
height={500}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</List>
);
}
// 只渲染可见区域的元素,大大减少 Diff 计算量
面试要点
-
Diff 算法是什么: 比较新旧 Virtual DOM 树差异的算法,目标是最小化 DOM 操作
-
复杂度优化: 从 O(n³) 优化到 O(n),基于三个策略:
- 跨层移动很少
- 不同类型组件产生不同树
- 使用 key 优化列表
-
三个层级:
- Tree Diff:逐层对比
- Component Diff:不同类型直接替换
- Element Diff:使用 key 优化列表对比
-
Key 的作用: 帮助 React 识别元素,优化列表对比性能
-
优化建议:
- 使用稳定的 key
- 避免跨层移动节点
- 保持组件类型一致
- 使用 React.memo
- 长列表使用虚拟列表