React 虚拟 DOM 与 Diff 算法
虚拟 DOM 是 React 的核心概念,理解它的工作原理对于写出高性能 React 应用至关重要。
虚拟 DOM 基础
什么是虚拟 DOM
虚拟 DOM(Virtual DOM)是一个 JavaScript 对象树,表示真实 DOM 的结构:
jsx// JSX const element = ( <div className="container"> <h1>标题</h1> <p>内容</p> </div> ); // 虚拟 DOM 结构(简化) { type: 'div', props: { className: 'container' }, children: [ { type: 'h1', props: {}, children: ['标题'] }, { type: 'p', props: {}, children: ['内容'] } ] }
为什么需要虚拟 DOM
直接操作 DOM 是昂贵的,虚拟 DOM 通过以下方式优化:
- 批量更新:将多次 DOM 操作合并为一次
- 差异化比较:只更新变化的部分
- 跨平台能力:抽象层支持 React Native 等平台
React 的协调过程
Reconciliation(协调)
当组件状态变化时,React 会执行协调过程:
状态变化 → 重新渲染组件 → 生成新的虚拟DOM树 → Diff算法比较 → 最小化更新真实DOM
React 的渲染流程
javascript// 1. 组件状态变化 this.setState({ count: this.state.count + 1 }); // 2. React 标记需要重新渲染 // 3. 生成新的虚拟 DOM 树 // 4. 新旧虚拟 DOM 比较(Diff) // 5. 计算出最小更新操作 // 6. 批量更新真实 DOM
Diff 算法
Diff 算法的三大策略
React Diff 算法基于三个关键策略:
1. Web UI 中 DOM 节点跨层级操作很少
jsx// 不推荐:跨层级移动 <A> <B /> </A> // 变为 <A> <C> <B /> </C> </A> // React 会:删除 B,重新创建 B(性能开销大)
2. 两个不同类型的元素产生不同的树
jsx// 元素类型改变,整个子树重新渲染 <div>...</div> // 变为 <span>...</span> // React 会销毁旧的 div 及所有子节点,重新创建
3. 开发者可以通过 key 标识稳定节点
jsx// 使用 key 帮助 React 识别节点 <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul>
对比类型:Tree Diff
同层级节点比较,基于类型判断:
jsx// 如果节点类型相同,只更新属性 <div className="old">内容</div> <div className="new">内容</div> // → 只更新 className // 如果节点类型不同,替换整个子树 <div>旧内容</div> <span>新内容</span> // → 销毁 div 及其所有子节点,创建新的 span
对比类型:Component Diff
组件类型比较规则:
jsx// 相同类型的组件: // 1. 先更新组件实例的 props // 2. 调用 componentWillReceiveProps // 3. 调用 componentWillUpdate // 4. 重新执行 render // 5. 进行 Tree Diff // 不同类型的组件: // → 销毁旧组件,创建新组件 // → componentWillUnmount → componentWillMount → componentDidMount
对比类型:Element Diff
列表元素比较,使用 key 优化:
jsx// 无 key:可能大量重排 [A, B, C] → [C, A, B] → 删除 C,创建 C(错误) // 有 key:正确识别移动 [A, B, C] → [C, A, B] → C 移动到开头(正确)
实战:性能优化
使用 React.memo 避免不必要的重渲染
jsxconst MemoizedComponent = React.memo(function MyComponent(props) { return <div>{props.value}</div>; }); // 自定义比较函数 const MemoizedWithCustomCompare = React.memo( MyComponent, (prevProps, nextProps) => { return prevProps.value === nextProps.value; } );
使用 useMemo 和 useCallback
jsxfunction ExpensiveComponent({ a, b }) { // 只在 a 或 b 变化时重新计算 const result = useMemo(() => { return computeExpensiveValue(a, b); }, [a, b]); return <div>{result}</div>; } function Parent() { const [count, setCount] = useState(0); // 只在 callback 变化时返回新的函数引用 const handleClick = useCallback(() => { console.log('clicked'); }, []); return <ExpensiveComponent onClick={handleClick} />; }
列表使用稳定的 key
jsx// ❌ 错误:使用索引作为 key {todos.map((todo, index) => ( <TodoItem key={index} todo={todo} /> ))} // ✅ 正确:使用唯一 ID {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} /> ))} // ⚠️ 可接受但不推荐:无重复数据的简单列表 {todos.map(todo => ( <TodoItem key={todo.text} todo={todo} /> ))}
避免内联对象和函数
jsx// ❌ 错误:每次渲染创建新对象 render() { return <Child style={{ color: 'red' }} />; } // ✅ 正确:将样式提取为常量 const childStyle = { color: 'red' }; render() { return <Child style={childStyle} />; } // ✅ 正确:使用 useMemo render() { const style = useMemo(() => ({ color: 'red' }), []); return <Child style={style} />; }
常见面试问题
Q1: 虚拟 DOM 的优缺点?
优点:
- 减少直接 DOM 操作,提高性能
- 批量更新,减少重排重绘
- 跨平台(React Native、Catalyst)
- 组件化开发体验好
缺点:
- 首次渲染可能比直接 DOM 慢
- 内存占用增加
- 复杂应用中 Diff 算法本身也有开销
Q2: React 为什么要求 key 要稳定?
key 帮助 React 识别哪些元素改变了、添加了或删除了。如果 key 不稳定,React 无法正确跟踪元素:
jsx// ❌ 问题:key 是随机生成的,每次都不同 {todos.map(todo => ( <TodoItem key={Math.random()} todo={todo} /> ))} // 结果:React 认为所有元素都是新的,执行销毁+重建
Q3: React Fiber 是什么?
React Fiber 是 React 16 引入的新的协调引擎:
- 可中断渲染:将渲染工作拆分成小单元,可以中断和恢复
- 优先级调度:高优先级更新(如用户输入)可以打断低优先级更新
- 并发模式基础:支持 Suspense、Concurrent Rendering
Q4: shouldComponentUpdate 的作用?
jsxclass Counter extends Component { shouldComponentUpdate(nextProps, nextState) { // 只有当 count 变化时才重新渲染 return nextProps.count !== this.props.count; } render() { return <div>{this.props.count}</div>; } } // 推荐使用 React.PureComponent 或 React.memo class Counter extends PureComponent { render() { return <div>{this.props.count}</div>; } }
总结
虚拟 DOM 和 Diff 算法是 React 高性能的核心:
- Tree Diff:同层级比较,基于类型判断
- Component Diff:相同类型组件更新 props,不同类型销毁重建
- Element Diff:列表元素使用 key 识别移动
性能优化原则:
- 保持稳定的 key
- 避免不必要的重渲染
- 使用 React.memo、useMemo、useCallback