🎨 什么是 Virtual DOM?
📖 定义
Virtual DOM 是真实 DOM 的轻量级副本,是一个只存在于内存中的 JavaScript 对象。像 React 这样的库使用 Virtual DOM 先在虚拟环境中处理 UI 变化,然后只将最小的变更应用到真实 DOM。这大大提高了 Web 应用程序的性能。
🎯 通过比喻理解
建筑蓝图比喻
将 Virtual DOM 比喻为建筑蓝图:
真实建筑 (Real DOM)
├─ 建筑成本高
├─ 修改耗时长
└─ 拆除重建困难
蓝图 (Virtual DOM)
├─ 可在纸上自由修改
├─ 快速且便宜
├─ 可比较多个方案
└─ 只将最终方案应用到真实建筑
流程:
1. 在蓝图上多次修改 (Virtual DOM)
2. 识别变更部分
3. 只对真实建筑应用最小变更 (Real DOM)
这样可以节省建筑成本和时间!
文字处理器比喻
记事本 (原生 JavaScript)
- 每次输入都立即保存
- 缓慢且低效
- 重复文件 I/O
文字处理器 (React + Virtual DOM)
- 在内存中编辑
- 跟踪变更
- 只在按保存按钮时写入文件
- 快速且高效
Virtual DOM = 内存缓冲区
Real DOM = 保存到磁盘
游戏渲染比喻
在游戏开发中:
直接绘制到屏幕 (直接操作 Real DOM)
- 会出现闪烁
- 性能下降
- 难以部分更新
双缓冲 (Virtual DOM 方式)
1. 在后台屏幕(缓冲区)绘制
2. 完成后与前台屏幕交换
3. 流畅渲染
4. 只更新变更的像素
Virtual DOM 起到与双缓冲相同的作用!
⚙️ 工作原理
1. 什么是 DOM?
// DOM (文档对象模型)
// 允许用 JavaScript 操作 HTML 的接口
<!DOCTYPE html>
<html>
<body>
<div id="root">
<h1>Hello</h1>
<p>World</p>
</div>
</body>
</html>
// DOM 树
document
└─ html
└─ body
└─ div#root
├─ h1 ("Hello")
└─ p ("World")
// 用 JavaScript 操作 DOM
document.getElementById('root').innerHTML = '<h1>Changed!</h1>';
2. Real DOM 的问题
// ❌ 低效的 Real DOM 操作
// 问题 1: 每次都完全 重新渲染
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // 1000次 DOM 操作!
}
// 问题 2: 布局重新计算 (Reflow)
element.style.width = '100px'; // 发生 Reflow
element.style.height = '100px'; // 发生 Reflow
element.style.margin = '10px'; // 发生 Reflow
// 问题 3: 复杂的 UI 更新
// 即使只改变一个字符也要重新创建整个组件
<div>
<Header />
<Content /> ← 只有这个改变了
<Footer />
</div>
// 必须全部重新创建!
// Real DOM 操作慢的原因:
// 1. 浏览器渲染引擎和 JavaScript 引擎之间的通信
// 2. Reflow (布局重新计算)
// 3. Repaint (重新绘制屏幕)
3. Virtual DOM 操作流程
// ✅ Virtual DOM 的高效处理
// 步骤 1: 初始渲染
const initialVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 0'] },
{ type: 'button', props: {}, children: ['Click'] }
]
};
// 反映到 Real DOM
<div id="app">
<h1>Count: 0</h1>
<button>Click</button>
</div>
// 步骤 2: 状态改变 (count = 1)
const newVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 1'] }, // ← 已改变
{ type: 'button', props: {}, children: ['Click'] }
]
};
// 步骤 3: Diffing (计算差异)
const diff = {
path: ['div', 'h1', 'text'],
oldValue: 'Count: 0',
newValue: 'Count: 1'
};
// 步骤 4: Reconciliation (协调)
// 只将最小变更应用到 Real DOM
document.querySelector('h1').textContent = 'Count: 1';
// div 和 button 保持不变!
4. Diffing 算法
// React 的 Diffing 算法原理
// 规则 1: 不同类型的元素产生不同的树
// 之前
<div>
<Counter />
</div>
// 之后
<span>
<Counter />
</span>
// 结果: 完全替换 div 为 span
// Counter 也会卸载后重新挂载
// 规则 2: 使用 key 追踪列表项
// ❌ 没有 key
<ul>
<li>A</li>
<li>B</li>
</ul>
// 在开头添加 C
<ul>
<li>C</li> // 重新创建全部
<li>A</li>
<li>B</li>
</ul>
// ✅ 使用 key
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// 在开头添加 C
<ul>
<li key="c">C</li> // 只添加 C
<li key="a">A</li> // 保持不变
<li key="b">B</li> // 保持不变
</ul>
// 规则 3: 递归比较子元素
function diff(oldNode, newNode) {
// 1. 类型不同则替换
if (oldNode.type !== newNode.type) {
return { action: 'REPLACE', newNode };
}
// 2. 比较属性
const propsDiff = diffProps(oldNode.props, newNode.props);
// 3. 比较子元素 (递归)
const childrenDiff = diffChildren(
oldNode.children,
newNode.children
);
return { propsDiff, childrenDiff };
}
5. Reconciliation (协调)
// Reconciliation: 将 Virtual DOM 反映到 Real DOM
// 阶段 1: Render Phase (异步)
// - 比较 Virtual DOM
// - 计算变更
// - 可中断 (React 18 Fiber)
// 阶段 2: Commit Phase (同步)
// - 更新 Real DOM
// - 执行生命周期方法
// - 不可中断
// 示例: 列表更新
const oldList = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
];
const newList = [
{ id: 1, name: 'A' },
{ id: 3, name: 'C-updated' }, // 修改
{ id: 4, name: 'D' } // 添加
// id: 2 删除
];
// Reconciliation 结果:
// - id:1 保留
// - id:2 移除
// - id:3 只更新文本
// - id:4 添加
// 最小化实际 DOM 操作:
// remove(id:2)
// updateText(id:3, 'C-updated')
// append(id:4)
💡 实际示例
基本 Virtual DOM 实现
// 简单的 Virtual DOM 实现 (教学用)
// 1. 创建 Virtual DOM 节点
function createElement(type, props = {}, ...children) {
return {
type,
props,
children: children.flat()
};
}
// 不使用 JSX
const vdom = createElement(
'div',
{ id: 'app', className: 'container' },
createElement('h1', {}, 'Hello Virtual DOM'),
createElement('p', {}, 'This is a paragraph')
);
console.log(vdom);
// {
// type: 'div',
// props: { id: 'app', className: 'container' },
// children: [
// { type: 'h1', props: {}, children: ['Hello Virtual DOM'] },
// { type: 'p', props: {}, children: ['This is a paragraph'] }
// ]
// }
// 2. 将 Virtual DOM 转换为 Real DOM
function render(vnode) {
// 文本节点
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(vnode);
}
// 元素节点
const element = document.createElement(vnode.type);
// 应用属性
Object.entries(vnode.props || {}).forEach(([key, value]) => {
if (key === 'className') {
element.setAttribute('class', value);
} else if (key.startsWith('on')) {
// 事件监听器
const event = key.substring(2).toLowerCase();
element.addEventListener(event, value);
} else {
element.setAttribute(key, value);
}
});
// 渲染子元素 (递归)
(vnode.children || []).forEach(child => {
element.appendChild(render(child));
});
return element;
}
// 使用
const domElement = render(vdom);
document.body.appendChild(domElement);
Diffing 算法实现
// 3. Diffing 算法 (简化版)
function diff(oldVNode, newVNode) {
// 1. 如果新节点不存在则移除
if (!newVNode) {
return { type: 'REMOVE' };
}
// 2. 如果旧节点不存在则添加
if (!oldVNode) {
return { type: 'CREATE', newVNode };
}
// 3. 如果类型不同则替换
if (
typeof oldVNode !== typeof newVNode ||
(typeof oldVNode === 'string' && oldVNode !== newVNode) ||
oldVNode.type !== newVNode.type
) {
return { type: 'REPLACE', newVNode };
}
// 4. 如果是相同类型则更新
if (newVNode.type) {
return {
type: 'UPDATE',
props: diffProps(oldVNode.props, newVNode.props),
children: diffChildren(oldVNode.children, newVNode.children)
};
}
// 5. 无变化
return { type: 'NONE' };
}
// 比较属性
function diffProps(oldProps = {}, newProps = {}) {
const patches = [];
// 已更改或添加的属性
Object.keys(newProps).forEach(key => {
if (oldProps[key] !== newProps[key]) {
patches.push({ type: 'SET_PROP', key, value: newProps[key] });
}
});
// 已移除的属性
Object.keys(oldProps).forEach(key => {
if (!(key in newProps)) {
patches.push({ type: 'REMOVE_PROP', key });
}
});
return patches;
}
// 比较子元素
function diffChildren(oldChildren = [], newChildren = []) {
const patches = [];
const maxLength = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLength; i++) {
patches.push(diff(oldChildren[i], newChildren[i]));
}
return patches;
}