Virtual DOM 与 Diff 算法
理解 Virtual DOM 原理、Diff 算法实现及 React key 的作用
问题
解释 Virtual DOM 的工作原理,说明 Diff 算法如何实现 O(n) 复杂度,以及 React 中 key 的作用。
解答
什么是 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: {},
children: ['Hello']
}
]
}
简易 Virtual DOM 实现
// 创建虚拟节点
function createElement(tag, props, ...children) {
return {
tag,
props: props || {},
children: children.flat()
}
}
// 渲染为真实 DOM
function render(vnode) {
// 文本节点
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(vnode)
}
const el = document.createElement(vnode.tag)
// 设置属性
for (const [key, value] of Object.entries(vnode.props)) {
if (key.startsWith('on')) {
// 事件处理
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
// 递归渲染子节点
vnode.children.forEach(child => {
el.appendChild(render(child))
})
return el
}
Diff 算法实现
传统树 Diff 复杂度是 O(n³),React 通过三个策略将其优化到 O(n):
- 同层比较:只比较同一层级的节点
- 类型判断:不同类型的节点直接替换
- key 标识:通过 key 识别节点身份
function diff(oldVNode, newVNode) {
// 新节点不存在,删除
if (!newVNode) {
return { type: 'REMOVE' }
}
// 旧节点不存在,新增
if (!oldVNode) {
return { type: 'CREATE', newVNode }
}
// 类型不同,替换
if (typeof oldVNode !== typeof newVNode ||
(typeof oldVNode === 'string' && oldVNode !== newVNode) ||
oldVNode.tag !== newVNode.tag) {
return { type: 'REPLACE', newVNode }
}
// 同类型元素节点,比较属性和子节点
if (newVNode.tag) {
return {
type: 'UPDATE',
props: diffProps(oldVNode.props, newVNode.props),
children: diffChildren(oldVNode.children, newVNode.children)
}
}
// 无变化
return null
}
// 比较属性
function diffProps(oldProps, newProps) {
const patches = []
// 检查新增和修改的属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
patches.push({ type: 'SET', key, value: newProps[key] })
}
}
// 检查删除的属性
for (const key in oldProps) {
if (!(key in newProps)) {
patches.push({ type: 'REMOVE', key })
}
}
return patches
}
// 比较子节点(带 key 优化)
function diffChildren(oldChildren, newChildren) {
const patches = []
// 构建 key -> index 映射
const oldKeyMap = new Map()
oldChildren.forEach((child, index) => {
if (child.props?.key != null) {
oldKeyMap.set(child.props.key, index)
}
})
newChildren.forEach((newChild, newIndex) => {
const key = newChild.props?.key
let oldIndex = -1
if (key != null && oldKeyMap.has(key)) {
// 通过 key 找到对应旧节点
oldIndex = oldKeyMap.get(key)
} else {
// 无 key,按索引匹配
oldIndex = newIndex < oldChildren.length ? newIndex : -1
}
const oldChild = oldIndex >= 0 ? oldChildren[oldIndex] : null
patches.push({
index: newIndex,
patch: diff(oldChild, newChild)
})
})
return patches
}
应用补丁
function patch(parent, patches, index = 0) {
const el = parent.childNodes[index]
switch (patches.type) {
case 'CREATE':
parent.appendChild(render(patches.newVNode))
break
case 'REMOVE':
parent.removeChild(el)
break
case 'REPLACE':
parent.replaceChild(render(patches.newVNode), el)
break
case 'UPDATE':
// 更新属性
patches.props.forEach(p => {
if (p.type === 'SET') {
el.setAttribute(p.key, p.value)
} else {
el.removeAttribute(p.key)
}
})
// 更新子节点
patches.children.forEach(childPatch => {
patch(el, childPatch.patch, childPatch.index)
})
break
}
}
key 的作用演示
// 没有 key:删除第一项时,React 认为是依次修改
// [A, B, C] -> [B, C]
// 实际操作:A->B, B->C, 删除C(3次操作)
// 有 key:React 能识别节点身份
// [A:1, B:2, C:3] -> [B:2, C:3]
// 实际操作:删除A(1次操作)
// 错误用法:用 index 作为 key
{items.map((item, index) => (
<Item key={index} data={item} /> // ❌ 列表变化时会出问题
))}
// 正确用法:用唯一标识作为 key
{items.map(item => (
<Item key={item.id} data={item} /> // ✅
))}
关键点
- Virtual DOM 本质:用 JS 对象描述 DOM,减少直接操作真实 DOM 的开销
- O(n) 复杂度:通过同层比较、类型判断、key 标识三个策略实现
- key 的作用:帮助 Diff 算法识别节点身份,实现最小化 DOM 操作
- key 的选择:使用稳定唯一的标识(如 id),避免使用数组索引
- 批量更新:Virtual DOM 可以将多次修改合并,一次性更新真实 DOM
目录