说说 React 中的虚拟 DOM 的理解?
问题解析
面试官考察点:
- 是否理解虚拟 DOM 的概念和作用
- 是否了解虚拟 DOM 的工作原理
- 是否理解 Diff 算法
- 对 React 性能优化机制的理解
核心概念
什么是虚拟 DOM
虚拟 DOM(Virtual DOM)本质上是以 JavaScript 对象形式存在的对真实 DOM 的描述。它是对真实 DOM 的抽象表示,通过对象树的形式来描述 UI 结构。
// JSX
const element = (
<div className="container">
<h1>Hello World</h1>
<p>Welcome to React</p>
</div>
);
// 编译后(虚拟 DOM 对象)
const element = React.createElement(
'div',
{ className: 'container' },
React.createElement('h1', null, 'Hello World'),
React.createElement('p', null, 'Welcome to React')
);
// 虚拟 DOM 对象结构
{
type: 'div',
props: {
className: 'container',
children: [
{
type: 'h1',
props: { children: 'Hello World' }
},
{
type: 'p',
props: { children: 'Welcome to React' }
}
]
}
}
为什么需要虚拟 DOM
- 跨平台能力:虚拟 DOM 是平台无关的,可以渲染到 Web、Native、Canvas 等不同平台
- 性能优化:通过 Diff 算法最小化 DOM 操作
- 开发体验:声明式编程,开发者只需关注状态变化
详细解答
1. 虚拟 DOM 的创建
// React.createElement 实现简化版
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object' ? child : createTextElement(child)
)
}
};
}
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
};
}
// 使用
const element = createElement(
'div',
{ id: 'app' },
createElement('h1', null, 'Hello')
);
2. 虚拟 DOM 转真实 DOM
// 渲染虚拟 DOM 到真实 DOM
function render(element, container) {
const dom =
element.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(element.type);
// 设置属性
Object.keys(element.props)
.filter(key => key !== 'children')
.forEach(name => {
dom[name] = element.props[name];
});
// 递归渲染子元素
element.props.children.forEach(child =>
render(child, dom)
);
container.appendChild(dom);
}
3. Diff 算法
React 使用 Diff 算法比较新旧虚拟 DOM,找出最小变更集。
Diff 策略
// 1. 同层比较(Tree Diff)
// React 只对同一层次的节点进行比较
// 如果节点跨层级移动,React 不会尝试复用,而是删除旧节点,创建新节点
// 旧树
<div>
<A />
</div>
// 新树
<span>
<A />
</span>
// A 会被销毁并重新创建,因为父节点从 div 变成了 span
// 2. 组件比较(Component Diff)
// 如果是同一类型的组件,继续比较 Virtual DOM
// 如果是不同类型的组件,直接替换整个组件
// 同一类型组件
<OldComponent /> -> <OldComponent /> // 继续比较
// 不同类型组件
<OldComponent /> -> <NewComponent /> // 直接替换
// 3. 元素比较(Element Diff)
// 对于同一层级的子节点,通过 key 来标识
// 旧列表
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// 新列表
<ul>
<li key="b">B</li>
<li key="a">A</li>
<li key="c">C</li>
</ul>
// 通过 key 识别,只需移动 A 和 B,插入 C
Diff 算法实现简化版
function diff(oldVNode, newVNode) {
// 1. 类型不同,直接替换
if (oldVNode.type !== newVNode.type) {
return { type: 'REPLACE', newVNode };
}
// 2. 文本节点
if (newVNode.type === 'TEXT_ELEMENT') {
if (oldVNode.props.nodeValue !== newVNode.props.nodeValue) {
return { type: 'TEXT', content: newVNode.props.nodeValue };
}
return null;
}
// 3. 比较属性
const patches = [];
const oldProps = oldVNode.props;
const newProps = newVNode.props;
// 删除旧属性
for (let key in oldProps) {
if (!(key in newProps)) {
patches.push({ type: 'REMOVE_PROP', key });
}
}
// 添加/更新属性
for (let key in newProps) {
if (oldProps[key] !== newProps[key]) {
patches.push({ type: 'SET_PROP', key, value: newProps[key] });
}
}
// 4. 比较子节点
const childrenPatches = diffChildren(
oldProps.children,
newProps.children
);
return { type: 'UPDATE', patches, childrenPatches };
}
function diffChildren(oldChildren, newChildren) {
const patches = [];
const maxLen = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLen; i++) {
patches.push(diff(oldChildren[i], newChildren[i]));
}
return patches;
}
4. Key 的作用
// 没有 key 的情况
// 旧:[A, B, C]
// 新:[B, C, D]
// 比较过程:
// A -> B (不同,更新)
// B -> C (不同,更新)
// C -> D (不同,更新)
// 删除多余的
// 有 key 的情况
// 旧:[A(key=1), B(key=2), C(key=3)]
// 新:[B(key=2), C(key=3), D(key=4)]
// 比较过程:
// 通过 key 识别,A 被删除,B 和 C 保持不变,D 被添加
5. Fiber 架构
React 16 引入了 Fiber 架构,将渲染工作拆分为小单元,支持可中断和恢复。
// Fiber 节点结构
{
type: 'div', // 组件类型
key: null, // key
props: {}, // props
stateNode: domNode, // 对应的真实 DOM
// 链表结构
child: fiber, // 第一个子节点
sibling: fiber, // 下一个兄弟节点
return: fiber, // 父节点
// 副作用
effectTag: 'UPDATE', // 标记需要执行的操作
nextEffect: fiber, // 下一个需要处理的副作用
}
深入理解
虚拟 DOM vs 真实 DOM
| 特性 | 虚拟 DOM | 真实 DOM |
|---|---|---|
| 类型 | JavaScript 对象 | 浏览器 API |
| 操作成本 | 低 | 高 |
| 跨平台 | 是 | 否 |
| 内存占用 | 额外开销 | 正常 |
| 首次渲染 | 较慢(需要计算) | 较快 |
虚拟 DOM 的优势
// 1. 批量更新
function BatchUpdate() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 多次 setState 会合并为一次 DOM 更新
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
// 最终只触发一次 DOM 更新
};
}
// 2. 跨平台渲染
// React DOM -> 浏览器
// React Native -> 移动端
// React Three Fiber -> 3D 场景
虚拟 DOM 的局限性
// 1. 不适合极高频更新
function HighFrequencyUpdate() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
// 高频更新会导致虚拟 DOM 计算开销
const interval = setInterval(() => {
setPosition(prev => ({
x: prev.x + 1,
y: prev.y + 1
}));
}, 16);
return () => clearInterval(interval);
}, []);
// 这种情况下,直接操作 DOM 可能更高效
}
// 2. 简单操作可能直接操作 DOM 更快
// 如只修改一个文本节点,虚拟 DOM 的计算可能比直接修改更慢
最佳实践
1. 合理使用 Key
// 推荐:使用稳定的唯一标识
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// 不推荐:使用索引作为 key(在列表顺序变化时会导致问题)
function BadList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
);
}
2. 避免不必要的渲染
// 使用 React.memo 减少不必要的虚拟 DOM 计算
const MemoComponent = React.memo(function MyComponent({ data }) {
return <div>{data.name}</div>;
});
// 使用 useMemo 缓存计算结果
function ExpensiveComponent({ items }) {
const processedItems = useMemo(() => {
return items.map(item => expensiveOperation(item));
}, [items]);
return <List items={processedItems} />;
}
3. 组件拆分
// 合理拆分组件,减少虚拟 DOM 比较范围
function App() {
return (
<div>
<Header />
<MainContent />
<Footer />
</div>
);
}
// 当 MainContent 更新时,Header 和 Footer 不会重新渲染
面试要点
- 虚拟 DOM 的本质:JavaScript 对象,描述真实 DOM 的结构
- 工作原理:创建虚拟 DOM -> Diff 比较 -> 生成补丁 -> 更新真实 DOM
- Diff 策略:同层比较、组件比较、元素比较(key)
- 优势:跨平台、性能优化(批量更新)、声明式编程
- 局限性:不适合极高频更新、简单操作可能直接操作 DOM 更快
常见追问:
- 虚拟 DOM 一定比直接操作 DOM 快吗?
- React 的 Diff 算法时间复杂度是多少?
- 为什么需要 key?key 的作用是什么?
- Fiber 架构解决了什么问题?