一、核心流程图
二、核心代码解析
1. 入口函数:patchChildren
(runtime-core/src/renderer.ts)
const patchChildren: PatchChildrenFn = (/*...*/) => {
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
patchKeyedChildren(/*...*/)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
patchUnkeyedChildren(/*...*/)
return
}
}
// ...其他处理逻辑
}
2. 核心算法:patchKeyedChildren
(runtime-core/src/renderer.ts)
实现步骤:
function patchKeyedChildren(
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1. 双端预处理
// (a) 从头部同步
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
if (isSameVNodeType(n1, n2)) {
patch(/*...*/)
} else {
break
}
i++
}
// (b) 从尾部同步
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
patch(/*...*/)
} else {
break
}
e1--
e2--
}
// 2. 处理新增节点
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor
while (i <= e2) {
patch(null, c2[i], container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
i++
}
}
}
// 3. 处理删除节点
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 4. 复杂序列处理
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5. 建立新节点 key 映射
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = c2[i]
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 6. 最长递增子序列优化
let j = 0
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
// ...中间处理逻辑...
// 7. 计算最长递增子序列(LIS)
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// 8. 移动/新增节点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex]
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 新增节点
patch(null, nextChild, container, anchor, /*...*/)
} else {
if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
// 移动节点
hostInsert(nextChild.el!, container, anchor)
} else {
j--
}
}
}
}
}
}
三、关键优化点解析
1. 双端对比策略
// 头部对比
while (i <= e1 && i <= e2) { /*...*/ }
// 尾部对比
while (i <= e1 && i <= e2) { /*...*/ }
- 时间复杂度:O(n) 快速跳过首尾相同节点
2. 最长递增子序列(LIS)
const getSequence = (arr: number[]): number[] => {
// ...使用贪心+二分查找算法实现...
}
- 作用:找到不需要移动的最长连续节点序列
- 复杂度:O(n log n)
3. Key映射优化
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
keyToNewIndexMap.set(c2[i].key, i)
}
- 优势:O(1) 时间复杂度查找节点位置
四、性能对比(Vue2 vs Vue3)
场景 | Vue2 双端对比 | Vue3 快速路径 + LIS |
---|---|---|
纯头部插入 | O(n) | O(1) |
尾部追加 | O(n) | O(1) |
随机插入 | O(n) | O(n) |
相同节点乱序 | O(n^2) | O(n log n) |
静态节点提升 | 无 | O(0) |
五、调试技巧
// 在浏览器控制台观察虚拟DOM结构
const app = createApp({/*...*/})
app.config.globalProperties.$logVNode = (vnode) => {
console.log(JSON.stringify(vnode, (k, v) => {
if (k === 'el') return undefined // 过滤DOM元素
return v
}, 2))
}
// 组件内使用
this.$logVNode(this._vnode)
六、性能优化实践
<!-- 1. 使用稳定的key -->
<div v-for="item in list" :key="item.id">
<!-- 2. 避免索引作为key -->
<!-- Bad -->
<div v-for="(item, index) in list" :key="index">
<!-- 3. 使用v-show替代v-if -->
<div v-show="shouldShow">
<!-- 4. 冻结静态数据 -->
<script>
const staticList = Object.freeze([/*...*/])
</script>
通过源码分析可以看出,Vue3 的 diff 算法通过以下策略实现性能突破:
- 双端预处理快速处理头尾变更
- Map结构实现快速节点查找
- 最长递增子序列减少DOM移动操作
- 静态提升跳过静态节点比对
- 补丁标志实现快速路径判断