React 面试题详细答案 - 第 37 题
37. React 的 Diffing 算法大致原理?(同层比较、key 优化、组件类型比对)
Diffing 算法概述
React 的 Diffing 算法是虚拟 DOM 的核心,用于比较新旧虚拟 DOM 树,找出需要更新的最小变化集,然后只更新真实 DOM 中发生变化的部分。
为什么需要 Diffing 算法?
// 没有 Diffing 算法的问题
// 每次状态更新都重新渲染整个 DOM 树
function updateDOM() {
// 清空整个 DOM
document.body.innerHTML = ''
// 重新创建所有元素
renderAllComponents()
}
// 有 Diffing 算法的优势
// 只更新发生变化的部分
function updateDOM() {
const changes = diff(oldVDOM, newVDOM)
applyChanges(changes) // 只更新变化的部分
}
三大核心策略
1. 同层比较策略
React 只比较同一层级的节点,不会跨层级比较。
// 示例:同层比较
// 旧树
<div>
<ComponentA />
<ComponentB />
</div>
// 新树
<div>
<ComponentA />
<ComponentC /> // 只比较这一层
</div>
// React 会:
// 1. 比较 div 标签(相同,不更新)
// 2. 比较第一个子节点 ComponentA(相同,不更新)
// 3. 比较第二个子节点 ComponentB vs ComponentC(不同,替换)
2. 组件类型比对
不同类型的组件会被完全替换。
// 组件类型不同,完全重新渲染
function OldComponent() {
return <div>Old</div>
}
function NewComponent() {
return <span>New</span>
}
// React 会:
// 1. 卸载 OldComponent
// 2. 挂载 NewComponent
// 3. 不会尝试复用任何内容
3. Key 优化策略
使用 key 来标识列表中的元素,帮助 React 识别哪些元素发生了变化。
// 没有 key 的问题
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li>{todo.text}</li> // 没有 key
))}
</ul>
)
}
// 有 key 的优化
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li> // 使用 key
))}
</ul>
)
}
Diffing 算法详细流程
1. 元素类型比较
// 伪代码:元素类型比较
function diffElement(oldElement, newElement) {
// 类型不同,完全替换
if (oldElement.type !== newElement.type) {
return {
type: 'REPLACE',
oldElement,
newElement,
}
}
// 类型相同,比较属性和子元素
return {
type: 'UPDATE',
props: diffProps(oldElement.props, newElement.props),
children: diffChildren(oldElement.children, newElement.children),
}
}
2. 属性比较
// 属性比较逻辑
function diffProps(oldProps, newProps) {
const changes = []
// 检查新增和修改的属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
changes.push({
type: 'SET_PROP',
key,
value: newProps[key],
})
}
}
// 检查删除的属性
for (const key in oldProps) {
if (!(key in newProps)) {
changes.push({
type: 'REMOVE_PROP',
key,
})
}
}
return changes
}
3. 子元素比较
// 子元素比较(简化版)
function diffChildren(oldChildren, newChildren) {
const changes = []
const maxLength = Math.max(oldChildren.length, newChildren.length)
for (let i = 0; i < maxLength; i++) {
const oldChild = oldChildren[i]
const newChild = newChildren[i]
if (!oldChild) {
// 新增子元素
changes.push({
type: 'INSERT',
index: i,
element: newChild,
})
} else if (!newChild) {
// 删除子元素
changes.push({
type: 'REMOVE',
index: i,
})
} else {
// 递归比较子元素
const childChanges = diffElement(oldChild, newChild)
if (childChanges) {
changes.push({
type: 'UPDATE',
index: i,
changes: childChanges,
})
}
}
}
return changes
}
Key 的作用机制
1. 没有 Key 的问题
// 问题示例:列表重排序
const oldList = [<div>Item 1</div>, <div>Item 2</div>, <div>Item 3</div>]
const newList = [
<div>Item 3</div>, // 移动到第一位
<div>Item 1</div>,
<div>Item 2</div>,
]
// 没有 key 时,React 会:
// 1. 比较第一个元素:Item 1 vs Item 3(不同,替换)
// 2. 比较第二个元素:Item 2 vs Item 1(不同,替换)
// 3. 比较第三个元素:Item 3 vs Item 2(不同,替换)
// 结果:3 个 DOM 元素都被替换,性能差
2. 有 Key 的优化
// 优化示例:使用 key
const oldList = [
<div key="1">Item 1</div>,
<div key="2">Item 2</div>,
<div key="3">Item 3</div>,
]
const newList = [
<div key="3">Item 3</div>, // 移动到第一位
<div key="1">Item 1</div>,
<div key="2">Item 2</div>,
]
// 有 key 时,React 会:
// 1. 找到 key="3" 的元素,移动到第一位
// 2. 找到 key="1" 的元素,移动到第二位
// 3. 找到 key="2" 的元素,移动到第三位
// 结果:只是 DOM 元素移动,没有重新创建,性能好
3. Key 的最佳实践
// 好的 key 选择
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li> // 使用唯一 ID
))}
</ul>
)
}
// 避免使用数组索引作为 key(在列表会变化时)
function BadExample({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li> // 不推荐
))}
</ul>
)
}
// 当没有唯一 ID 时,可以组合多个属性
function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<li key={`${product.category}-${product.name}`}>{product.name}</li>
))}
</ul>
)
}
组件级别的 Diffing
1. 函数组件比较
// 函数组件:比较函数引用
function MyComponent({ name }) {
return <div>Hello {name}</div>
}
// 如果函数组件没有变化,React 会跳过重新渲染
// 如果 props 没有变化,React 会跳过重新渲染
2. 类组件比较
// 类组件:比较类引用
class MyComponent extends React.Component {
render() {
return <div>Hello {this.props.name}</div>
}
}
// React 会:
// 1. 比较类组件类型
// 2. 比较 props
// 3. 如果都相同,跳过重新渲染
3. 组件更新优化
// 使用 React.memo 优化函数组件
const MyComponent = React.memo(function MyComponent({ name, age }) {
return (
<div>
{name} is {age} years old
</div>
)
})
// 使用 PureComponent 优化类组件
class MyComponent extends React.PureComponent {
render() {
return <div>Hello {this.props.name}</div>
}
}
// 自定义比较函数
const MyComponent = React.memo(
function MyComponent({ user }) {
return <div>{user.name}</div>
},
(prevProps, nextProps) => {
// 返回 true 表示 props 相同,跳过重新渲染
return prevProps.user.id === nextProps.user.id
}
)
实际应用示例
1. 列表渲染优化
// 优化前:性能问题
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem
key={index} // 使用索引作为 key
todo={todo}
onToggle={onToggle}
/>
))}
</ul>
)
}
// 优化后:使用唯一 key
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id} // 使用唯一 ID
todo={todo}
onToggle={onToggle}
/>
))}
</ul>
)
}
// 进一步优化:使用 React.memo
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</li>
)
})
2. 条件渲染优化
// 条件渲染:使用 key 强制重新渲染
function UserProfile({ user, isEditing }) {
if (isEditing) {
return <EditForm key={user.id} user={user} />
}
return <DisplayProfile key={user.id} user={user} />
}
// 或者使用不同的组件类型
function UserProfile({ user, isEditing }) {
return isEditing ? <EditForm user={user} /> : <DisplayProfile user={user} />
}
3. 动态组件优化
// 动态组件:使用 key 确保正确更新
function DynamicComponent({ type, props }) {
const components = {
button: Button,
input: Input,
select: Select,
}
const Component = components[type]
return <Component key={type} {...props} />
}
// 避免在渲染中创建新对象
function BadExample({ items }) {
return (
<div>
{items.map((item) => (
<Item
key={item.id}
item={item}
onClick={() => handleClick(item)} // 每次渲染都创建新函数
/>
))}
</div>
)
}
// 优化:使用 useCallback
function GoodExample({ items }) {
const handleClick = useCallback((item) => {
// 处理点击
}, [])
return (
<div>
{items.map((item) => (
<Item key={item.id} item={item} onClick={handleClick} />
))}
</div>
)
}
性能优化技巧
1. 避免不必要的重新渲染
// 使用 useMemo 缓存计算结果
function ExpensiveComponent({ data, filter }) {
const filteredData = useMemo(() => {
return data.filter((item) => item.category === filter)
}, [data, filter])
return (
<div>
{filteredData.map((item) => (
<Item key={item.id} item={item} />
))}
</div>
)
}
// 使用 useCallback 缓存函数
function ParentComponent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
setCount((prev) => prev + 1)
}, [])
return <ChildComponent onClick={handleClick} />
}
2. 合理使用 key
// 在列表变化时使用稳定的 key
function SortableList({ items, sortBy }) {
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a[sortBy] - b[sortBy])
}, [items, sortBy])
return (
<ul>
{sortedItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
// 在组件状态重置时使用 key
function Form({ initialData }) {
const [data, setData] = useState(initialData)
return (
<form key={initialData.id}>
{' '}
{/* 当 initialData.id 变化时,表单会重置 */}
<input
value={data.name}
onChange={(e) => setData({ ...data, name: e.target.value })}
/>
</form>
)
}
总结
React 的 Diffing 算法通过以下策略实现高效更新:
- 同层比较:只比较同一层级的节点,避免跨层级比较的复杂性
- 组件类型比对:不同类型的组件直接替换,相同类型的组件进行属性比较
- Key 优化:使用 key 标识列表元素,实现最小化 DOM 操作
关键优化点:
- 使用稳定的、唯一的 key
- 避免在渲染中创建新对象和函数
- 合理使用 React.memo、useMemo、useCallback
- 理解组件更新时机,避免不必要的重新渲染
掌握这些原理和技巧,可以显著提升 React 应用的性能。