实现一个简易的虚拟DOM
实现一个简易的虚拟 DOM 需要分三步走:
- 用 JS 对象描述 DOM 结构
- 对比新旧虚拟 DOM 树的差异
- 将差异批量更新到真实 DOM
以下是分步说明及核心代码示例。
一、用 JS 对象描述 DOM 结构(Virtual Node)
// 定义虚拟节点类型
function createElement(type, props, ...children) {
return {
type, // 标签名(如 'div')
props, // 属性(含 children)
key: props?.key || null, // 唯一标识(用于 diff 优化)
children: children.flat().map(child =>
typeof child === 'object' ? child : createTextVNode(child)
)
};
}
// 处理文本节点
function createTextVNode(text) {
return {
type: 'TEXT_ELEMENT',
props: { nodeValue: text, children: [] }
};
}
// 使用示例
const virtualDOM = createElement('div', { id: 'app' },
createElement('h1', { style: 'color: red' }, 'Hello'),
createElement('p', null, 'Virtual DOM Demo')
);
二、差异对比算法(Diffing)
核心策略:
- 同层比较
- 组件级跳过
- 元素级 Key 匹配
function diff(oldVNode, newVNode) {
const patches = []; // 存储差异补丁
dfsDiff(oldVNode, newVNode, 0, patches);
return patches;
}
function dfsDiff(oldNode, newNode, index, patches) {
if (!newNode) {
patches.push({ type: 'REMOVE', index });
return;
}
if (isSameNodeType(oldNode, newNode)) {
// 属性差异对比
const propsPatches = diffProps(oldNode.props, newNode.props);
if (propsPatches.length) {
patches.push({ type: 'PROPS', index, props: propsPatches });
}
// 递归对比子节点
diffChildren(oldNode.children, newNode.children, patches);
} else {
patches.push({ type: 'REPLACE', newNode });
}
}
function diffProps(oldProps, newProps) {
const patches = [];
// 对比样式、class、事件等属性变化...
return patches;
}
function diffChildren(oldChildren, newChildren, patches) {
// 实现双端比较或最长递增子序列算法(此处简化)
const diff = listDiff(oldChildren, newChildren, 'key');
diff.forEach(patch => {
const index = patch.index;
if (patch.type === 'INSERT') {
patches.push({ type: 'INSERT', newNode: patch.node, index });
} else if (patch.type === 'MOVE') {
patches.push({ type: 'MOVE', from: patch.from, to: index });
}
});
}
三、应用差异到真实 DOM(Patching)
function patch(node, patches) {
const walker = { index: 0 };
dfsWalk(node, walker, patches);
}
function dfsWalk(node, walker, patches) {
const currentPatches = patches[walker.index];
// 应用属性补丁
currentPatches?.props.forEach(propPatch => {
applyPropPatch(node, propPatch);
});
// 应用子节点补丁
let childIndex = 0;
const children = Array.isArray(node.childNodes) ? node.childNodes : [];
while (childIndex < children.length) {
dfsWalk(children[childIndex], walker, patches);
childIndex++;
}
// 应用替换/插入/移动操作
currentPatches?.forEach(patch => {
switch (patch.type) {
case 'REPLACE':
const newNode = patch.newNode.type === 'TEXT_ELEMENT'
? document.createTextNode(patch.newNode.props.nodeValue)
: document.createElement(patch.newNode.type);
node.parentNode.replaceChild(newNode, node);
break;
case 'INSERT':
const insertNode = createRealNode(patch.newNode);
node.insertBefore(insertNode, node.childNodes[patch.index] || null);
break;
case 'MOVE':
const movingNode = node.removeChild(node.childNodes[patch.from]);
node.insertBefore(movingNode, node.childNodes[patch.to] || null);
break;
case 'REMOVE':
node.removeChild(node.childNodes[patch.index]);
break;
}
});
}
四、关键优化策略
1. Key 的妙用
// 列表对比时通过 key 快速定位节点
function listDiff(oldList, newList, key) {
const keyMap = new Map();
newList.forEach(node => {
const nodeKey = node.props[key];
if (nodeKey !== undefined) keyMap.set(nodeKey, node);
});
// 实现移动/插入/删除逻辑...
}
2. 批量更新策略
// 使用 requestIdleCallback 实现异步更新
function scheduleUpdate(patches) {
requestIdleCallback(() => {
applyPatches(patches);
});
}
3. 事件委托优化
// 在 patch 阶段自动绑定事件监听
function applyPropPatch(node, propPatch) {
if (propPatch.type === 'EVENT') {
node.addEventListener(propPatch.name, propPatch.handler);
}
}
五、完整使用示例
// 1. 首次渲染
const realDOM = render(virtualDOM);
document.body.appendChild(realDOM);
// 2. 更新虚拟DOM
const newVirtualDOM = createElement('div', { id: 'app' },
createElement('h1', { style: 'color: blue' }, 'Hello'),
createElement('p', { key: 'p' }, 'Updated Content')
);
// 3. 计算差异并打补丁
const patches = diff(virtualDOM, newVirtualDOM);
patch(realDOM, patches);
六、实现要点总结
- 分层对比:通过深度优先遍历逐层比较,避免全局遍历
- 组件隔离:遇到组件边界时直接跳过子树对比
- 移动优化:对列表操作使用 LIS(最长递增子序列)算法减少 DOM 操作
- 批处理机制:将多个更新合并为一次 DOM 操作
实际框架(如 React)的实现复杂度更高,包含:
- Fiber 架构实现可中断的异步渲染
- 优先级调度系统
- 并发模式(Concurrent Mode)
- 更精细的副作用管理
这个简化实现能帮助理解核心原理,但要用于生产环境还需处理边界条件、添加类型系统、优化性能等。