虚拟 DOM(Virtual DOM) 是一种通过用 JavaScript 对象来模拟真实 DOM 的机制。它可以提高网页的性能和响应速度,减少直接对 DOM 的操作,从而优化 UI 渲染。
虚拟 DOM 的原理
创建虚拟 DOM:
- 使用 JavaScript 对象来表示 DOM 元素。虚拟 DOM 结构类似于真实 DOM,但它是轻量级的,可以在内存中快速操作。
比较差异(Diffing):
- 当组件状态或属性发生变化时,生成一个新的虚拟 DOM 树。
- 通过对比新旧虚拟 DOM 树,找到差异(diff)。
更新真实 DOM:
- 将差异应用到真实 DOM 上,进行最小化的更新,以提高性能。
手写一个简单的虚拟 DOM 实现
以下是一个简化版的虚拟 DOM 实现示例:
// 创建虚拟 DOM 节点
function createElement(tag, props, ...children) {
return {
tag,
props: props || {},
children: children.flat().map(child =>
typeof child === 'object' ? child : createTextNode(child)
)
};
}
// 创建虚拟 DOM 文本节点
function createTextNode(text) {
return { tag: 'TEXT', props: { nodeValue: text } };
}
// 渲染虚拟 DOM 节点到真实 DOM
function render(vnode, container) {
if (vnode.tag === 'TEXT') {
const textNode = document.createTextNode(vnode.props.nodeValue);
container.appendChild(textNode);
return;
}
const element = document.createElement(vnode.tag);
Object.keys(vnode.props).forEach(prop => {
element[prop] = vnode.props[prop];
});
vnode.children.forEach(child => render(child, element));
container.appendChild(element);
}
// 比较两个虚拟 DOM 树,更新差异
function diff(oldVNode, newVNode) {
if (!oldVNode) {
return newVNode;
}
if (!newVNode) {
return null;
}
if (oldVNode.tag !== newVNode.tag) {
return newVNode;
}
if (oldVNode.tag === 'TEXT') {
if (oldVNode.props.nodeValue !== newVNode.props.nodeValue) {
return newVNode;
}
return null;
}
const patch = {};
const propChanges = Object.keys(newVNode.props).reduce((acc, prop) => {
if (oldVNode.props[prop] !== newVNode.props[prop]) {
acc[prop] = newVNode.props[prop];
}
return acc;
}, {});
if (Object.keys(propChanges).length > 0) {
patch.props = propChanges;
}
const childPatches = [];
const maxLength = Math.max(oldVNode.children.length, newVNode.children.length);
for (let i = 0; i < maxLength; i++) {
const childPatch = diff(oldVNode.children[i], newVNode.children[i]);
if (childPatch) {
childPatches[i] = childPatch;
}
}
if (childPatches.length > 0) {
patch.children = childPatches;
}
return Object.keys(patch).length > 0 ? patch : null;
}
// 应用虚拟 DOM 差异到真实 DOM
function patch(node, patch) {
if (patch === null) {
return;
}
if (patch.props) {
Object.keys(patch.props).forEach(prop => {
node[prop] = patch.props[prop];
});
}
if (patch.children) {
patch.children.forEach((childPatch, index) => {
const childNode = node.childNodes[index];
if (childNode) {
patch(childNode, childPatch);
}
});
}
}
// 使用示例
const oldVNode = createElement('div', { id: 'container' },
createElement('h1', null, 'Hello, World!'),
createElement('p', null, 'This is a virtual DOM example.')
);
const newVNode = createElement('div', { id: 'container' },
createElement('h1', null, 'Hello, Virtual DOM!'),
createElement('p', null, 'Updated example.')
);
const container = document.getElementById('app');
render(oldVNode, container);
setTimeout(() => {
const patch = diff(oldVNode, newVNode);
patch(container, patch);
}, 2000);
解释
createElement
:创建虚拟 DOM 节点。
createTextNode
:创建虚拟文本节点。
render
:将虚拟 DOM 渲染到真实 DOM。
diff
:对比虚拟 DOM 树,找出差异。
patch
:将差异应用到真实 DOM 上。