当我们在使用 Vue.js 开发应用时,Vue 内部会通过一种称为 “Virtual DOM” 的机制来进行 DOM 的更新,其中的关键部分是 “diff 算法”。Diff 算法的目标是在新旧 Virtual DOM 树之间找到最小的差异,以最小化对实际 DOM 的更新操作,提高性能。
什么是 虚拟DOM(Virtual DOM)?
Virtual DOM 是一个虚拟的浏览器内存中的 DOM 表示。它是由 JavaScript 对象构成的树形结构,模拟了实际的 DOM 结构。当你修改页面的时候,Vue 不会直接操作实际的 DOM,而是先操作 Virtual DOM,然后通过 “Diff 算法” 找出变化,最后才更新实际的 DOM。
简单理解一下虚拟Dom:
渲染页面的Dom操作等同于拼图游戏。实际Dom就是你的实体拼图,虚拟Dom则是拼图图纸,当你想要对拼图上(真实Dom)的某一块进行更新时,先在图纸上(虚拟Dom)标记出来需要变化的位置,然后再进行更新/替换,这样就只需要对拼图的一小部分进行修改(Diff算法),而不需要重新拼一整个拼图(重新渲染虚拟Dom)
为什么使用虚拟Dom
- 性能优化: 直接修改实际 DOM 可能会触发浏览器的重新渲染,而虚拟 DOM 允许我们在内存中操作,最后一次性更新到实际 DOM,减少渲染次数。
- 方便比对: 虚拟 DOM 是一个轻量级的 JavaScript 对象,易于比对。这就像你用图纸比对实际拼图的过程,找到差异并进行精准修改。
Diff 算法如何工作?
Diff 算法有三个步骤:分析变化、比较差异、更新 DOM。
分析变化: Vue 会对比新旧两棵 Virtual DOM 树,找出哪些地方发生了变化,哪些地方需要更新。
比较差异: 通过一些巧妙的算法,Diff 算法会找出最小的差异,包括节点的增加、删除、移动等。
更新 DOM: 最后,Vue 会根据 Diff 的结果,只更新发生变化的部分,而不是整个页面。
有key和无key的Diff算法
Diff算法的核心思想是
无keyDiff算法
在没有使用 key 的情况下,如果列表中的元素位置发生变化,Vue 会按照新旧列表的索引顺序进行比较,而不关心元素具体的内容。
<!-- 无 Key 的情况 -->
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
<!-- 更新后的 DOM -->
<ul>
<li>D</li>
<li>A</li>
<li>C</li>
</ul>
- A 在新列表中的位置由第一个变为第二个,因此 A 的位置发生了变化。
- B 在新列表中已经不存在,所以需要删除。
- C 在新列表中仍然存在,并且位置保持不变,不需要更新。
Diff算法 无key源码
两个 VNode 数组进行 Diff 操作的函数。这个函数会对比旧的 VNode 数组 c1 和新的 VNode 数组 c2,然后在容器 container 中进行相应的 DOM 操作。
// 没有 key 的 diff 算法
const patchUnkeyedChildren = (
c1: VNode[], // 旧的 VNode 数组
c2: VNodeArrayChildren, // 新的 VNode 数组
container: RendererElement, // 容器
anchor: RendererNode | null, // 插入位置
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
let i
// 通过 for 循环对比新旧 VNode
for (i = 0; i < commonLength; i++) {
// 对于优化过的情况,如果节点已挂载,则克隆节点
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// 执行 patch 操作,更新或创建节点
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
if (oldLength > newLength) {
// 删除多余的旧节点
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength
)
} else {
// 添加新的节点
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
commonLength
)
}
}
核心工作是通过 for 循环遍历新旧 VNode 数组的共同部分,对每个对应位置的 VNode 进行 patch 操作。然后,根据新旧数组的长度差异,决定是删除多余的旧节点还是添加新的节点。
有key Diff算法
有 Key 的 Diff 算法同样在两个 VNode 数组进行比较,但它会借助 key 的信息更精准地更新节点。以下是这个算法的核心代码:
const patchKeyedChildren = (
c1: VNode[], // 旧的 VNode 数组
c2: VNodeArrayChildren, // 新的 VNode 数组
container: RendererElement, // 容器
parentAnchor: RendererNode | null, // 插入位置的锚点
parentComponent: ComponentInternalInstance | null, // 父组件实例
parentSuspense: SuspenseBoundary | null, // 父 Suspense 边界
isSVG: boolean, // 是否为 SVG 元素
slotScopeIds: string[] | null, // 插槽作用域的 ID 数组
optimized: boolean // 是否优化
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // 旧列表的结束索引
let e2 = l2 - 1 // 新列表的结束索引
// 1. 同步开始部分
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (isSameVNodeType(n1, n2)) {
// 如果是相同类型的节点,则执行 patch 操作
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 如果不是相同类型的节点,则终止循环
break
}
i++
}
// 2. 同步结束部分
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
// 如果是相同类型的节点,则执行 patch 操作
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 如果不是相同类型的节点,则终止循环
break
}
e1--
e2--
}
// 3. 处理公共部分
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
// 在旧列表中找不到对应的节点,则执行 mount 操作
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
// 4. 处理未知序列 - 删除多余的旧节点
else if (i > e2) {
while (i <= e1) {
// 删除多余的旧节点
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. 处理未知序列 - 处理新的节点
else {
const s1 = i // 旧列表的起始索引
const s2 = i // 新列表的起始索引
// 5.1 构建 key 到索引的映射关系
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
warn(
`Duplicate keys found during update:`,
JSON.stringify(nextChild.key),
`Make sure keys are unique.`
)
}
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 5.2 遍历未知序列
let j
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) {
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
// 5.3 处理未知序列中的移动和添加
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 在未知序列中找不到对应的旧节点,则执行 mount 操作
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// 如果发生了移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
// 如果不存在最长递增子序列或当前节点不在稳定序列中,执行移动操作
move(nextChild, container, anchor, MoveType.REORDER)
} else {
// 在稳定序列中,继续查找
j--
}
}
}
}
}