Virtual DOM实现详解
本文目标
学完本文,你将能够:
- 理解Virtual DOM的设计初衷和核心价值
- 掌握VNode节点的类型系统和数据结构
- 实现完整的Virtual DOM创建、比较和更新机制
- 理解createElement和patch函数的工作原理
- 掌握Virtual DOM的性能优化策略
系列导航
目录
为什么需要Virtual DOM
在传统的Web开发中,我们直接操作DOM来更新页面。然而,这种方式存在严重的性能问题:
// 传统DOM操作的性能问题演示
console.time('直接DOM操作');
const container = document.getElementById('app');
// 更新1000个列表项
for (let i = 0; i < 1000; i++) {
const item = container.children[i];
if (item) {
// 每次操作都会触发重排和重绘
item.textContent = `Item ${i} - Updated`;
item.style.color = i % 2 === 0 ? 'red' : 'blue';
}
}
console.timeEnd('直接DOM操作');
DOM操作的性能瓶颈
- 频繁的重排(Reflow)和重绘(Repaint)
- DOM API调用开销大
- 缺乏批量更新机制
- 难以进行差异化更新
Virtual DOM的解决方案
Virtual DOM通过在JavaScript和真实DOM之间建立一个虚拟层,将多次DOM操作转换为一次批量更新:
VNode的设计与实现
VNode类型系统
Virtual DOM的核心是VNode(虚拟节点),它是真实DOM节点的JavaScript对象表示:
// VNode的核心类型定义
class VNode {
constructor(
tag, // 标签名或组件
data, // 节点数据(属性、事件等)
children, // 子节点
text, // 文本内容
elm, // 对应的真实DOM
context, // 组件实例
key // 节点标识
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.context = context;
this.key = data && data.key;
this.componentOptions = undefined;
this.componentInstance = undefined;
this.parent = undefined;
// 标记节点类型
this.isStatic = false;
this.isComment = false;
this.isCloned = false;
}
}
// VNode类型枚举
const VNodeTypes = {
ELEMENT: 'ELEMENT', // 元素节点
TEXT: 'TEXT', // 文本节点
COMPONENT: 'COMPONENT', // 组件节点
FUNCTIONAL: 'FUNCTIONAL', // 函数式组件
COMMENT: 'COMMENT', // 注释节点
FRAGMENT: 'FRAGMENT' // 片段节点
};
VNode工厂函数
为了方便创建不同类型的VNode,我们实现一系列工厂函数:
// 创建元素VNode
function createElementVNode(tag, data, children) {
return new VNode(tag, data, normalizeChildren(children));
}
// 创建文本VNode
function createTextVNode(text) {
return new VNode(undefined, undefined, undefined, String(text));
}
// 创建注释VNode
function createCommentVNode(text) {
const node = new VNode();
node.text = text;
node.isComment = true;
return node;
}
// 创建空VNode
function createEmptyVNode() {
const node = new VNode();
node.text = '';
node.isComment = true;
return node;
}
// 克隆VNode
function cloneVNode(vnode) {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions
);
cloned.key = vnode.key;
cloned.isCloned = true;
return cloned;
}
// 规范化子节点
function normalizeChildren(children) {
if (typeof children === 'string') {
return [createTextVNode(children)];
} else if (Array.isArray(children)) {
return children.map(child => {
if (typeof child === 'string') {
return createTextVNode(child);
}
return child;
});
}
return [];
}
createElement函数详解
createElement函数(在Vue中也称为h函数)是创建VNode的核心API:
// 完整的createElement实现
function createElement(context, tag, data, children) {
// 参数规范化
if (Array.isArray(data)) {
children = data;
data = undefined;
}
if (!tag) {
return createEmptyVNode();
}
// 处理组件
if (typeof tag === 'object') {
return createComponentVNode(tag, data, children, context);
}
// 处理普通HTML标签
if (typeof tag === 'string') {
// 保留标签处理
if (isReservedTag(tag)) {
return createElementVNode(
tag,
data,
normalizeChildren(children)
);
}
// 组件标签处理
const Ctor = resolveAsset(context, 'components', tag);
if (Ctor) {
return createComponentVNode(Ctor, data, children, context);
}
// 未知标签
return createElementVNode(tag, data, children);
}
// 处理函数式组件
if (typeof tag === 'function') {
return createFunctionalComponent(tag, data, children, context);
}
}
// 处理VNode数据
function processVNodeData(data) {
const processedData = {};
// 处理class
if (data.class) {
processedData.class = normalizeClass(data.class);
}
// 处理style
if (data.style) {
processedData.style = normalizeStyle(data.style);
}
// 处理attributes
if (data.attrs) {
processedData.attrs = data.attrs;
}
// 处理事件
if (data.on) {
processedData.on = data.on;
}
// 处理props
if (data.props) {
processedData.props = data.props;
}
// 处理directives
if (data.directives) {
processedData.directives = data.directives;
}
return processedData;
}
// 使用示例
const vnode = createElement('div', {
class: 'container',
style: { color: 'red' },
on: { click: handleClick }
}, [
createElement('h1', 'Hello Virtual DOM'),
createElement('p', 'This is a paragraph'),
createElement('ul', [
createElement('li', 'Item 1'),
createElement('li', 'Item 2'),
createElement('li', 'Item 3')
])
]);
patch算法核心实现
patch算法是Virtual DOM的核心,负责将VNode渲染为真实DOM,并高效地更新DOM:
// patch函数主体
function patch(oldVnode, vnode, parentElm) {
// 如果新节点不存在,删除旧节点
if (!vnode) {
if (oldVnode) removeVnodes([oldVnode], 0, 0);
return;
}
// 如果旧节点不存在,创建新节点
if (!oldVnode) {
createElm(vnode, parentElm);
return;
}
// 判断是否为相同节点
if (sameVnode(oldVnode, vnode)) {
// 更新现有节点
patchVnode(oldVnode, vnode);
} else {
// 替换节点
const oldElm = oldVnode.elm;
const parentElm = oldElm.parentNode;
createElm(vnode, parentElm, oldElm.nextSibling);
removeVnodes([oldVnode], 0, 0);
}
return vnode.elm;
}
// 判断是否为相同节点
function sameVnode(a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
);
}
// 创建真实DOM
function createElm(vnode, parentElm, refElm) {
// 尝试创建组件
if (createComponent(vnode, parentElm, refElm)) {
return;
}
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;
if (tag) {
// 创建元素节点
vnode.elm = document.createElement(tag);
// 设置属性
setAttrs(vnode);
// 创建子节点
createChildren(vnode, children);
// 插入DOM
insert(parentElm, vnode.elm, refElm);
} else if (vnode.isComment) {
// 创建注释节点
vnode.elm = document.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
// 创建文本节点
vnode.elm = document.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
// 更新节点
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) return;
const elm = vnode.elm = oldVnode.elm;
const oldCh = oldVnode.children;
const ch = vnode.children;
// 更新属性
updateAttrs(oldVnode, vnode);
// 更新文本内容
if (vnode.text) {
if (oldVnode.text !== vnode.text) {
elm.textContent = vnode.text;
}
} else {
if (oldCh && ch) {
// 更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch);
} else if (ch) {
// 添加新子节点
if (oldVnode.text) elm.textContent = '';
addVnodes(elm, null, ch, 0, ch.length - 1);
} else if (oldCh) {
// 删除旧子节点
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (oldVnode.text) {
// 清空文本
elm.textContent = '';
}
}
}
// 高效的子节点更新算法
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let newEndIdx = newCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// 双端比较算法
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 旧头 vs 新头
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 旧尾 vs 新尾
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 旧头 vs 新尾
patchVnode(oldStartVnode, newEndVnode);
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 旧尾 vs 新头
patchVnode(oldEndVnode, newStartVnode);
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 使用key进行查找
if (!oldKeyToIdx) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key];
if (!idxInOld) {
// 新节点
createElm(newStartVnode, parentElm, oldStartVnode.elm);
} else {
// 移动节点
vnodeToMove = oldCh[idxInOld];
patchVnode(vnodeToMove, newStartVnode);
oldCh[idxInOld] = undefined;
parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm);
}
newStartVnode = newCh[++newStartIdx];
}
}
// 处理剩余节点
if (oldStartIdx > oldEndIdx) {
// 添加剩余的新节点
refElm = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx);
} else if (newStartIdx > newEndIdx) {
// 删除剩余的旧节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
性能分析与优化
性能测试对比
让我们通过实际测试来比较Virtual DOM和直接DOM操作的性能差异:
// 性能测试工具类
class PerformanceTester {
constructor() {
this.results = [];
}
// 测试直接DOM操作
testDirectDOM(count = 1000) {
const container = document.createElement('div');
document.body.appendChild(container);
const start = performance.now();
// 初始创建
for (let i = 0; i < count; i++) {
const div = document.createElement('div');
div.className = 'item';
div.textContent = `Item ${i}`;
container.appendChild(div);
}
// 批量更新
const items = container.querySelectorAll('.item');
items.forEach((item, i) => {
item.textContent = `Updated Item ${i}`;
item.style.color = i % 2 === 0 ? 'red' : 'blue';
});
const end = performance.now();
document.body.removeChild(container);
return {
name: 'Direct DOM',
time: end - start,
operations: count * 2
};
}
// 测试Virtual DOM
testVirtualDOM(count = 1000) {
const container = document.createElement('div');
document.body.appendChild(container);
const start = performance.now();
// 初始渲染
const oldVnode = createElement('div', null,
Array.from({ length: count }, (_, i) =>
createElement('div', { class: 'item' }, `Item ${i}`)
)
);
patch(null, oldVnode, container);
// 批量更新
const newVnode = createElement('div', null,
Array.from({ length: count }, (_, i) =>
createElement('div', {
class: 'item',
style: { color: i % 2 === 0 ? 'red' : 'blue' }
}, `Updated Item ${i}`)
)
);
patch(oldVnode, newVnode, container);
const end = performance.now();
document.body.removeChild(container);
return {
name: 'Virtual DOM',
time: end - start,
operations: count * 2
};
}
// 运行性能测试
runBenchmark(counts = [100, 500, 1000, 5000]) {
console.log('开始性能测试...\n');
counts.forEach(count => {
console.log(`测试规模: ${count} 个节点`);
const directResult = this.testDirectDOM(count);
const virtualResult = this.testVirtualDOM(count);
console.log(`Direct DOM: ${directResult.time.toFixed(2)}ms`);
console.log(`Virtual DOM: ${virtualResult.time.toFixed(2)}ms`);
console.log(`性能提升: ${((directResult.time - virtualResult.time) / directResult.time * 100).toFixed(2)}%\n`);
this.results.push({
count,
direct: directResult.time,
virtual: virtualResult.time
});
});
this.visualizeResults();
}
// 可视化结果
visualizeResults() {
console.log('性能对比图表:');
console.log('节点数量 | Direct DOM | Virtual DOM | 提升比例');
console.log('---------|------------|-------------|----------');
this.results.forEach(result => {
const improvement = ((result.direct - result.virtual) / result.direct * 100).toFixed(2);
console.log(
`${result.count.toString().padEnd(8)} | ` +
`${result.direct.toFixed(2).padEnd(10)}ms | ` +
`${result.virtual.toFixed(2).padEnd(11)}ms | ` +
`${improvement}%`
);
});
}
}
// 运行测试
const tester = new PerformanceTester();
// tester.runBenchmark();
Virtual DOM性能优化策略
1. 静态节点优化
// 标记静态节点,避免重复创建
function markStatic(node) {
node.isStatic = isStatic(node);
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
markStatic(child);
if (!child.isStatic) {
node.isStatic = false;
}
}
}
}
function isStatic(node) {
// 文本节点
if (node.type === 2) {
return true;
}
// 表达式节点
if (node.type === 3) {
return false;
}
// 元素节点
return !!(
!node.hasBindings && // 没有动态绑定
!node.if && !node.for && // 没有v-if/v-for
!isBuiltInTag(node.tag) && // 不是内置组件
isPlatformReservedTag(node.tag) && // 是平台保留标签
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
);
}
2. 组件级别的优化
// 组件级别的Virtual DOM优化
class OptimizedComponent {
constructor(options) {
this.options = options;
this._vnode = null;
this._staticTrees = null;
}
// 缓存静态子树
_renderStatic(index) {
const cached = this._staticTrees || (this._staticTrees = []);
let tree = cached[index];
if (tree) {
return Array.isArray(tree)
? cloneVNodes(tree)
: cloneVNode(tree);
}
// 渲染静态子树
tree = this.options.staticRenderFns[index].call(this);
cached[index] = tree;
return tree;
}
// 优化的更新策略
_update(vnode) {
const prevVnode = this._vnode;
this._vnode = vnode;
if (!prevVnode) {
// 初始渲染
this.$el = this._patch(null, vnode);
} else {
// 更新:只对比组件级别的变化
this.$el = this._patch(prevVnode, vnode);
}
}
// 智能的shouldUpdate检查
shouldUpdate(nextProps, nextState) {
// 浅比较props和state
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
}
3. key值优化策略
// 优化的key-index映射
function createKeyToOldIdx(children, beginIdx, endIdx) {
const map = {};
for (let i = beginIdx; i <= endIdx; i++) {
const key = children[i].key;
if (key !== undefined) {
map[key] = i;
}
}
return map;
}
// 使用最长递增子序列优化移动
function getSequence(arr) {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
实战应用与最佳实践
1. 合理使用key属性
// 错误:使用索引作为key
const list = items.map((item, index) =>
createElement('li', { key: index }, item.text)
);
// 正确:使用唯一且稳定的标识
const list = items.map(item =>
createElement('li', { key: item.id }, item.text)
);
// 展示key的重要性
class KeyDemo {
constructor() {
this.items = [
{ id: 1, text: 'Apple' },
{ id: 2, text: 'Banana' },
{ id: 3, text: 'Cherry' }
];
}
// 演示错误的key使用
renderWithIndexKey() {
return createElement('ul',
this.items.map((item, index) =>
createElement('li', {
key: index,
style: { transition: 'all 0.3s' }
}, [
createElement('input', { type: 'text' }),
createElement('span', item.text)
])
)
);
}
// 演示正确的key使用
renderWithIdKey() {
return createElement('ul',
this.items.map(item =>
createElement('li', {
key: item.id,
style: { transition: 'all 0.3s' }
}, [
createElement('input', { type: 'text' }),
createElement('span', item.text)
])
)
);
}
// 重新排序演示
shuffle() {
this.items = this.items.sort(() => Math.random() - 0.5);
// 使用index作为key时,input的值会错乱
// 使用id作为key时,input的值会正确保持
}
}
2. 避免不必要的渲染
// 实现一个智能的组件更新系统
class SmartVirtualDOM {
constructor() {
this.components = new Map();
}
// 注册组件的渲染优化
registerComponent(name, component) {
this.components.set(name, {
component,
lastProps: null,
lastVNode: null,
renderCount: 0
});
}
// 智能渲染
renderComponent(name, props) {
const record = this.components.get(name);
if (!record) return null;
// 检查是否需要重新渲染
if (this.shouldComponentUpdate(record, props)) {
record.renderCount++;
record.lastProps = props;
record.lastVNode = record.component.render(props);
console.log(`Component ${name} rendered (${record.renderCount} times)`);
} else {
console.log(`Component ${name} render skipped`);
}
return record.lastVNode;
}
// 智能的更新检查
shouldComponentUpdate(record, newProps) {
if (!record.lastProps) return true;
// 深度比较优化
return !deepEqual(record.lastProps, newProps);
}
// 批量更新优化
batchUpdate(updates) {
console.log('开始批量更新...');
const startTime = performance.now();
// 收集所有需要更新的组件
const updateQueue = [];
updates.forEach(({ name, props }) => {
const record = this.components.get(name);
if (record && this.shouldComponentUpdate(record, props)) {
updateQueue.push({ name, props, record });
}
});
// 批量执行更新
updateQueue.forEach(({ name, props, record }) => {
record.renderCount++;
record.lastProps = props;
record.lastVNode = record.component.render(props);
});
const endTime = performance.now();
console.log(`批量更新完成,耗时: ${(endTime - startTime).toFixed(2)}ms`);
console.log(`更新组件数: ${updateQueue.length}/${updates.length}`);
}
}
// 使用示例
const smartVDOM = new SmartVirtualDOM();
// 注册组件
smartVDOM.registerComponent('UserCard', {
render(props) {
return createElement('div', { class: 'user-card' }, [
createElement('h3', props.name),
createElement('p', props.email)
]);
}
});
// 多次渲染测试
smartVDOM.renderComponent('UserCard', { name: 'John', email: '[email protected]' });
smartVDOM.renderComponent('UserCard', { name: 'John', email: '[email protected]' }); // 跳过
smartVDOM.renderComponent('UserCard', { name: 'Jane', email: '[email protected]' }); // 渲染
3. Virtual DOM的实际应用场景
// 完整的TodoList应用示例
class VirtualDOMTodoApp {
constructor(container) {
this.container = container;
this.state = {
todos: [],
filter: 'all', // all, active, completed
input: ''
};
this.vnode = null;
}
// 添加待办事项
addTodo(text) {
this.state.todos.push({
id: Date.now(),
text,
completed: false
});
this.render();
}
// 切换完成状态
toggleTodo(id) {
const todo = this.state.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.render();
}
}
// 删除待办事项
removeTodo(id) {
this.state.todos = this.state.todos.filter(t => t.id !== id);
this.render();
}
// 渲染应用
render() {
const newVnode = this.buildVNode();
if (this.vnode) {
// 更新现有DOM
patch(this.vnode, newVnode);
} else {
// 初始渲染
patch(null, newVnode, this.container);
}
this.vnode = newVnode;
}
// 构建Virtual DOM树
buildVNode() {
const { todos, filter, input } = this.state;
// 过滤待办事项
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return createElement('div', { class: 'todo-app' }, [
// 标题
createElement('h1', 'Virtual DOM Todo App'),
// 输入框
createElement('div', { class: 'todo-input' }, [
createElement('input', {
type: 'text',
placeholder: '添加待办事项...',
value: input,
on: {
input: (e) => {
this.state.input = e.target.value;
this.render();
},
keyup: (e) => {
if (e.key === 'Enter' && this.state.input.trim()) {
this.addTodo(this.state.input.trim());
this.state.input = '';
}
}
}
}),
createElement('button', {
on: {
click: () => {
if (this.state.input.trim()) {
this.addTodo(this.state.input.trim());
this.state.input = '';
}
}
}
}, '添加')
]),
// 过滤器
createElement('div', { class: 'filters' }, [
['all', 'active', 'completed'].map(f =>
createElement('button', {
class: filter === f ? 'active' : '',
on: {
click: () => {
this.state.filter = f;
this.render();
}
}
}, f.charAt(0).toUpperCase() + f.slice(1))
)
]),
// 待办事项列表
createElement('ul', { class: 'todo-list' },
filteredTodos.map(todo =>
createElement('li', {
key: todo.id,
class: todo.completed ? 'completed' : ''
}, [
createElement('input', {
type: 'checkbox',
checked: todo.completed,
on: {
change: () => this.toggleTodo(todo.id)
}
}),
createElement('span', todo.text),
createElement('button', {
on: {
click: () => this.removeTodo(todo.id)
}
}, '删除')
])
)
),
// 统计信息
createElement('div', { class: 'stats' }, [
createElement('span', `总计: ${todos.length}`),
createElement('span', `已完成: ${todos.filter(t => t.completed).length}`),
createElement('span', `未完成: ${todos.filter(t => !t.completed).length}`)
])
]);
}
}
// 初始化应用
// const app = new VirtualDOMTodoApp(document.getElementById('app'));
// app.render();
核心要点总结
-
Virtual DOM的本质:用JavaScript对象表示DOM结构,通过对比差异来最小化DOM操作
-
性能优势来源:
- 批量DOM更新
- 最小化重排重绘
- 高效的diff算法
- 编译时优化
-
设计权衡:
- 内存开销换取性能提升
- 开发效率与运行效率的平衡
- 简单场景可能不如直接操作DOM
-
最佳实践:
- 合理使用key属性
- 避免不必要的渲染
- 利用静态节点优化
- 组件级别的更新控制
-
适用场景:
- 复杂的交互式应用
- 频繁的数据更新
- 大量的DOM操作
- 需要跨平台渲染
相关文章
- Diff算法深度剖析 - 深入理解Virtual DOM的核心算法
- 模板编译详解 - 了解模板如何转换为Virtual DOM
- 异步更新机制 - 理解批量更新的实现原理
- 性能优化实践 - 基于Virtual DOM的优化技巧
最后更新: 2025年1月