Virtual DOM 的意义
理解 Virtual DOM 存在的原因和工作原理
问题
为什么使用 Virtual DOM?直接操作 DOM 不行吗?
解答
直接操作 DOM 的问题
DOM 操作是昂贵的。每次修改 DOM,浏览器可能需要:
- 重新计算样式(Recalculate Style)
- 重新布局(Layout/Reflow)
- 重新绘制(Paint)
- 合成图层(Composite)
// 糟糕的做法:频繁操作 DOM
for (let i = 0; i < 1000; i++) {
document.body.innerHTML += `<div>${i}</div>`; // 每次都触发重排重绘
}
// 稍好的做法:批量操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // 只触发一次
Virtual DOM 是什么
Virtual DOM 是用 JavaScript 对象描述 DOM 结构:
// 真实 DOM
// <div class="container">
// <span>Hello</span>
// </div>
// Virtual DOM
const vnode = {
tag: 'div',
props: { class: 'container' },
children: [
{
tag: 'span',
props: null,
children: ['Hello']
}
]
};
Virtual DOM 的工作流程
// 简化的 Virtual DOM 实现
function createElement(tag, props, ...children) {
return { tag, props, children: children.flat() };
}
// 渲染 Virtual DOM 到真实 DOM
function render(vnode) {
// 文本节点
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(vnode);
}
const el = document.createElement(vnode.tag);
// 设置属性
if (vnode.props) {
Object.entries(vnode.props).forEach(([key, value]) => {
el.setAttribute(key, value);
});
}
// 递归渲染子节点
vnode.children.forEach(child => {
el.appendChild(render(child));
});
return el;
}
// 使用
const vdom = createElement('div', { class: 'app' },
createElement('h1', null, 'Title'),
createElement('p', null, 'Content')
);
document.body.appendChild(render(vdom));
Diff 算法
Virtual DOM 的价值在于 diff:比较新旧两棵树,找出最小变更:
// 简化的 diff 实现
function diff(oldVNode, newVNode, parent, index = 0) {
const el = parent.childNodes[index];
// 新节点不存在,删除旧节点
if (!newVNode) {
parent.removeChild(el);
return;
}
// 旧节点不存在,添加新节点
if (!oldVNode) {
parent.appendChild(render(newVNode));
return;
}
// 节点类型不同,直接替换
if (oldVNode.tag !== newVNode.tag) {
parent.replaceChild(render(newVNode), el);
return;
}
// 文本节点,更新内容
if (typeof newVNode === 'string') {
if (oldVNode !== newVNode) {
el.textContent = newVNode;
}
return;
}
// 更新属性
updateProps(el, oldVNode.props, newVNode.props);
// 递归比较子节点
const maxLen = Math.max(
oldVNode.children.length,
newVNode.children.length
);
for (let i = 0; i < maxLen; i++) {
diff(oldVNode.children[i], newVNode.children[i], el, i);
}
}
function updateProps(el, oldProps = {}, newProps = {}) {
// 移除旧属性
Object.keys(oldProps).forEach(key => {
if (!(key in newProps)) {
el.removeAttribute(key);
}
});
// 设置新属性
Object.keys(newProps).forEach(key => {
if (oldProps[key] !== newProps[key]) {
el.setAttribute(key, newProps[key]);
}
});
}
Virtual DOM 的真正价值
// 假设有 1000 个列表项,只有第 500 个变了
const oldList = Array.from({ length: 1000 }, (_, i) => ({
tag: 'li',
props: null,
children: [`Item ${i}`]
}));
const newList = [...oldList];
newList[500] = { tag: 'li', props: null, children: ['Updated Item'] };
// Virtual DOM diff 后,只会更新第 500 个 <li>
// 而不是重新渲染整个列表
关键点
- 批量更新:Virtual DOM 收集多次状态变更,一次性计算出最小 DOM 操作
- 跨平台:同一套 Virtual DOM 可以渲染到 DOM、Canvas、Native 等不同平台
- 声明式编程:开发者只需描述 UI 应该是什么样,框架负责计算如何更新
- 性能并非最优:手动精细操作 DOM 可能更快,但 Virtual DOM 提供了性能与开发体验的平衡
- diff 有成本:Virtual DOM 本身的创建和 diff 也消耗性能,对于简单场景可能是负优化
目录