什么是虚拟 DOM?
虚拟 DOM(Virtual DOM)是对真实 DOM 的一种抽象表示,它是一个普通的 JavaScript 对象,包含了真实 DOM 节点的所有信息。通过操作虚拟 DOM,我们可以避免直接操作真实 DOM,从而提高性能
核心实现
1. 虚拟节点类 (VNode)
class VNode {
tag: string;
props: Record<string, any>;
children: VNode[] | string | null;
key: string | number | null;
text: string | null;
el: string | HTMLElement | null | Text;
constructor(
tag: string,
props = {},
children: Array<VNode> | string | null = [],
key = null,
text: string | null = null
) {
this.tag = tag;
this.props = props;
this.children = children;
this.key = key;
this.text = text;
this.el = null;
}
}VNode 类是虚拟 DOM 的核心,它包含了以下属性:
tag: 标签名(如 'div')props: 属性对象(如 { class: 'app' })children: 子节点(数组,元素为 VNode 或文本)key: 节点唯一标识,用于 diff 算法text: 文本节点内容(文本节点时生效)el: 对应的真实 DOM 元素(创建后挂载)
2. 节点创建函数
h 函数
export const h = function (
tag: string,
props: Record<string, any> = {},
children: Array<VNode> | string | null = [],
key = null
) {
if (typeof children === 'string' || typeof children === 'number') {
return new VNode(tag, props, [], key, (children as string | number).toString());
}
return new VNode(tag, props, children, key);
};h 函数是创建虚拟节点的工厂函数,它接受标签名、属性、子节点和 key 作为参数,返回一个新的 VNode 实例。
createTxtVNode 函数
export const createTxtVNode = (text: string) => {
return new VNode('', {}, [], null, text);
};createTxtVNode 函数专门用于创建文本虚拟节点。
3. 节点挂载
export const mount = function (
vnode: VNode,
container: HTMLElement,
anchor: HTMLElement | null = null
) {
if (vnode.text !== null) {
const textNode = document.createTextNode(vnode.text);
vnode.el = textNode;
container.insertBefore(textNode, anchor);
return;
}
const el = document.createElement(vnode.tag);
vnode.el = el;
// 属性挂载
Object.entries(vnode.props).forEach(([key, value]) => {
setProp(el, key, value);
});
// 子节点挂载
(vnode.children as VNode[]).forEach((child) => {
mount(child, el);
});
// 挂载到容器
container.insertBefore(el, anchor);
};mount 函数负责将虚拟节点挂载到真实 DOM 上,它的工作流程是:
如果是文本节点,创建文本节点并挂载
如果是元素节点,创建元素并设置属性
递归挂载子节点
将元素挂载到容器中
4. 属性设置
export const setProp = (el: HTMLElement, key: string, value: any) => {
if (key === 'class') {
el.className = value;
} else if (key === 'style') {
Object.entries(value as Record<string, string>).forEach(([k, v]) => {
if (v !== null && v !== undefined && v !== '') {
el.style.setProperty(k, v);
} else {
el.style.removeProperty(k);
}
});
} else if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase();
el.addEventListener(eventName, value);
} else {
el.setAttribute(key, value);
}
};setProp 函数负责设置元素的属性,支持类名、样式、事件等属性的设置。
5. 节点更新 (Diff 算法)
patch 函数
export const patch = function (oldVNode: VNode, newVNode: VNode, container?: HTMLElement | null) {
// 1. 节点类型不同直接挂载
if (
newVNode.tag !== oldVNode.tag ||
(newVNode.text !== null && oldVNode.text !== null && newVNode.text !== oldVNode.text)
) {
const oldEL = (oldVNode.el as HTMLElement) || container;
const parent = (oldEL?.parentNode as HTMLElement) || container;
mount(newVNode, parent);
if (oldVNode.el !== null) {
parent.removeChild(oldVNode.el as HTMLElement);
}
return;
}
// 2.对比文本内容
if (newVNode.text !== oldVNode.text) {
(oldVNode.el as HTMLElement).textContent = newVNode.text;
newVNode.el = oldVNode.el;
return;
}
// 3.复用真实DOM
newVNode.el = oldVNode.el;
const newEL = newVNode.el as HTMLElement;
// 4.对比属性
patchProps(newEL, oldVNode.props, newVNode.props);
// 5.对比子节点
patchChildren(newEL, oldVNode.children as VNode[], newVNode.children as VNode[]);
};patch 函数是 diff 算法的核心,它的工作流程是:
如果节点类型不同,直接创建新节点并替换旧节点
如果是文本节点,直接更新文本内容
复用真实 DOM 元素
对比并更新属性
对比并更新子节点
双端对比算法
export const patchListChildren = (EL: HTMLElement, oldChildren: VNode[], newChildren: VNode[]) => {
// 新旧节点列表首尾指针
let oldStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = newChildren.length - 1;
// 新列表 key 构建映射
const keyToOldIdxMap = new Map();
oldChildren.forEach((item, idx) => {
if (item.key !== null) {
keyToOldIdxMap.set(item.key, idx);
}
});
// 循环对比
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 获取节点
const oldStartVNode = oldChildren[oldStartIdx] as VNode;
const oldEndVNode = oldChildren[oldEndIdx] as VNode;
const newStartVNode = newChildren[newStartIdx] as VNode;
const newEndVNode = newChildren[newEndIdx] as VNode;
// 头头对比
if (oldStartVNode?.key === newStartVNode?.key) {
patch(oldStartVNode, newStartVNode, EL);
oldStartIdx++;
newStartIdx++;
continue;
}
// 尾尾对比
if (oldEndVNode?.key === newEndVNode?.key) {
patch(oldEndVNode, newEndVNode, EL);
oldEndIdx--;
newEndIdx--;
continue;
}
// 头尾对比
if (oldStartVNode.key === newEndVNode.key) {
patch(oldStartVNode, newEndVNode, EL);
EL.insertBefore(oldStartVNode.el as Node, (oldEndVNode.el as Node).nextSibling);
oldStartIdx++;
newEndIdx--;
continue;
}
// 尾头对比
if (oldEndVNode.key === newStartVNode.key) {
patch(oldEndVNode, newStartVNode, EL);
EL.insertBefore(oldEndVNode.el as Node, oldStartVNode.el as Node);
oldEndIdx--;
newStartIdx++;
continue;
}
// 其他: 默认使用 newStartVNode.key 查找
const oldTargetKeyIdx = keyToOldIdxMap.get(newStartVNode.key);
if (oldTargetKeyIdx !== undefined) {
// 找到旧节点 移动 DOM
const oldTargetVNode = oldChildren[oldTargetKeyIdx] as VNode;
patch(oldTargetVNode, newStartVNode, EL);
EL.insertBefore(oldTargetVNode.el as Node, oldStartVNode.el as Node);
} else {
// 未找到,创建新节点
mount(newStartVNode, EL, oldStartVNode.el as HTMLElement);
}
newStartIdx++;
}
// 剩余节点处理
// 新列表有剩余节点, 插入
while (newStartIdx <= newEndIdx) {
mount(newChildren[newStartIdx] as VNode, EL, oldChildren[oldStartIdx]?.el as HTMLElement);
newStartIdx++;
}
// 旧列表有剩余节点则删除
while (oldStartIdx <= oldEndIdx) {
EL.removeChild(oldChildren[oldStartIdx]?.el as Node);
oldStartIdx++;
}
};双端对比算法是 Vue 2.x 中使用的高效列表更新算法,它通过头尾指针的方式进行对比,减少 DOM 操作:
头头对比:如果新旧列表头部节点相同,直接更新
尾尾对比:如果新旧列表尾部节点相同,直接更新
头尾对比:如果旧列表头部节点与新列表尾部节点相同,移动节点到尾部
尾头对比:如果旧列表尾部节点与新列表头部节点相同,移动节点到头部
其他情况:使用 key 查找旧节点,找到则移动,未找到则创建新节点
处理剩余节点:新列表有剩余则插入,旧列表有剩余则删除
总结
虚拟 DOM 是 Vue 等现代前端框架的核心技术之一,它通过抽象真实 DOM,减少直接操作 DOM 的次数,从而提高应用性能。本实现展示了虚拟 DOM 的基本原理,包括虚拟节点的创建、挂载、更新等核心功能,以及双端对比算法的实现。
Vue 虚拟 DOM与双端Diff
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。




评论交流
欢迎留下你的想法