// 真实DOM操作代价示例
const start = Date.now()
for(let i=0; i<1000; i++){
document.createElement('div') // 真实DOM操作非常耗时
}
console.log('耗时:', Date.now()-start) // 约20-50ms
// 虚拟DOM操作示例
const vnodes = []
for(let i=0; i<100000; i++){ // 10万次操作
vnodes.push({ tag: 'div', data: {}, children: [] })
}
console.log('JS对象操作耗时极低') // <1ms
核心价值:
// 完整的VNode结构
{
tag: 'div', // 标签名/组件名
data: { // 属性/指令/事件等
attrs: { id: 'app' },
on: { click: handler }
},
children: [ // 子节点
{
tag: 'span',
text: 'Counter: ' + this.count
}
],
elm: document.createElement('div'), // 对应的真实DOM
key: 'unique_key', // 优化Diff的关键
context: vm // 组件上下文
}
关键属性解析:
tag
:标识节点类型(平台标签/组件)data
:包含所有节点属性/事件/指令等信息children
:子节点树结构key
:Diff算法的优化关键elm
:与真实DOM的映射关系算法约束:
key
标识可复用的节点代码实现:
function sameVnode(a, b) {
return (
a.key === b.key && // key相同
a.tag === b.tag && // 标签相同
a.isComment === b.isComment && // 注释节点
isDef(a.data) === isDef(b.data) && // 数据定义一致
sameInputType(a, b) // 相同的输入类型
)
}
四指针遍历策略:
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0
let oldEndIdx = oldCh.length - 1
let newStartIdx = 0
let newEndIdx = newCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 四种比较情况
if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1:头头相同
patchVnode(...)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2:尾尾相同
patchVnode(...)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3:头尾相同
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
patchVnode(...)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4:尾头相同
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
patchVnode(...)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 使用key的查找策略
const idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (idxInOld) {
// 移动已有节点
parentElm.insertBefore(oldCh[idxInOld].elm, oldStartVnode.elm)
} else {
// 创建新节点
createElm(newStartVnode, parentElm, oldStartVnode.elm)
}
newStartVnode = newCh[++newStartIdx]
}
}
// 处理剩余节点
if (oldStartIdx > oldEndIdx) {
addVnodes(...)
} else {
removeVnodes(...)
}
}
算法优势:
function patchVnode(oldVnode, newVnode) {
const elm = newVnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const newCh = newVnode.children
// 属性更新
if (newVnode.data) {
updateAttrs(elm, newVnode, oldVnode)
updateClass(elm, newVnode, oldVnode)
updateDOMListeners(elm, newVnode, oldVnode)
}
// 文本节点
if (newVnode.text) {
if (oldVnode.text !== newVnode.text) {
elm.textContent = newVnode.text
}
}
// 子节点更新
else {
if (oldCh && newCh) {
updateChildren(elm, oldCh, newCh) // 触发Diff算法
} else if (newCh) {
addVnodes(elm, null, newCh, 0, newCh.length-1)
} else if (oldCh) {
removeVnodes(elm, oldCh, 0, oldCh.length-1)
}
}
}
无key时的更新问题:
<li>Item 1li>
<li>Item 2li>
<li>Item 0li>
<li>Item 1li>
<li>Item 2li>
有key的正确处理:
<li key="a">Item 1li>
<li key="b">Item 2li>
<li key="c">Item 0li>
<li key="a">Item 1li>
<li key="b">Item 2li>
Key的最佳实践:
编译阶段优化:
// 原始模板
<div>
<h1>Static Title</h1> <!-- 静态节点 -->
<p>{{ dynamicContent }}</p>
</div>
// 优化后渲染函数
const _hoisted_1 = _c('h1', [_v("Static Title")])
function render() {
return _c('div', [
_hoisted_1, // 直接复用静态节点
_c('p', [_v(_s(dynamicContent))])
])
}
// 性能分析示例
const stats = {
totalNodes: 0,
created: 0,
removed: 0,
moved: 0
}
function patch() {
// ...在每次操作时更新统计信息
}
// 输出结果示例
console.log(`节点总数: ${stats.totalNodes}`)
console.log(`新建节点: ${stats.created}`)
console.log(`移动节点: ${stats.moved}`)
console.log(`删除节点: ${stats.removed}`)
优化指标:
深度实践建议:
// 性能测试案例
const vm = new Vue({
data: { items: ['A','B','C'] },
template: `
- {{ item }}
`
})
// 测试数据反转时的更新效率
vm.items.reverse() // 观察DOM操作次数