一、v-for必须要指定key,其作用是什么?
在源码中有一个函数为,其中就是通过判断两个vnode的type和key进行判断,如果这两个属性相同,那么这两个vnode就是相同,所以在设置key的时候也不可以设置为object等无法通过三等号判断的类型。
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
二、虚拟 DOM
Virtual DOM 本质上是 JavaScript 对象,是真实 DOM 的描述,用一个 JS 对象来描述一个 DOM 节点。
Virtual DOM 可以看做一棵模拟 DOM 树的 JavaScript 树,其主要是通过 VNode 实现一个无状态的组件,当组件状态发生更新时,然后触发 Virtual DOM 数据的变化,然后通过 Virtual DOM 和真实 DOM 的比对,再对真实 DOM 更新。可以简单认为 Virtual DOM 是真实 DOM 的缓存。
优点:
- 跨平台与分层设计(主要原因):虚拟 DOM 本质上是 JavaScript 对象,而真实 DOM 与平台强相关,相比之下虚拟 DOM 带来了分层设计、跨平台以及 SSR 等特性。至于 Virtual DOM 比 原生 DOM 谁的性能好,需要 “控制变量法” 才能比较。这是为什么要设计虚拟 DOM 的主要原因。虚拟 DOM 抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 iOS 的原生组件,也可以是小程序,也可以是各种 GUI。
- 以最小的代价更新变化的视图。整棵 DOM 树实现代价太高,能否只更新变化的部分的视图。虚拟 DOM 能通过 patch 准确地转换为真实 DOM,并且方便进行 diff。
- 保证性能下限(次要原因):框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作(分层设计),它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限。
- 无需手动操作 DOM:操作 DOM 慢,js 运行效率高。我们可以将 DOM 对比(diff 操作)放在 JS 层,提高效率。我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率。
- 组件的高度抽象化:Vue.2x 引入 VirtualDOM 把渲染过程抽象化,从而使得组件的抽象能力也得到提升,并且可以适配 DOM 以外的渲染目标。不再依赖 HTML 解析器进行模版解析,可以进行更多的 AOT 工作提高运行时效率:通过模版 AOT 编译,Vue 的运行时体积可以进一步压缩,运行时效率可以进一步提升。Virtual DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。为了实现高效的 DOM 操作,一套高效的虚拟 DOM diff 算法显得很有必要.
缺点:
- 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
- 虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 Virtual DOM 树。
- 在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费。
- 传统 Virtual DOM 的性能跟模版大小正相关,跟动态节点的数量无关。
三、diff算法,vue2 / 3 diff的区别
vue2的diff算法策略
diff 算法是一种通过同层的树节点进行比较的高效算法。diff 整体策略为:深度优先,同层比较
在 diff 比较的过程中,循环从两边向中间比较(vue 的双端比较法)
新旧两个 VNode 节点的左右头尾两侧均有一个变量标识,在遍历过程中这几个变量都会向中间靠拢。当 oldStartIdx <= oldEndIdx 或者 newStartIdx <= newEndIdx 时结束循环。在遍历中,如果存在 key,并且满足 sameVnode,会将该 DOM 节点进行复用(只通过移动节点顺序),否则则会创建一个新的 DOM 节点。
具体过程
当组件创建和更新时,vue会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom
对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程
在对比时,vue采用深度优先、同级比较的方式进行比对。同级比较就是说它不会跨越结构进行比较
在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
这样一直递归的遍历下去,直到整棵树完成对比。
Vue 2 和 Vue 3 在虚拟 DOM 的 diff 算法上有一些区别。以下是它们之间的主要区别:
- Vue 2 使用的是基于递归的双指针的 diff 算法,而 Vue 3 使用的是基于数组的动态规划的 diff 算法。Vue 3 的算法效率更高,因为它使用了一些优化技巧,例如按需更新、静态标记等。
- Vue 2 的 diff 算法会对整个组件树进行完整的遍历和比较,而 Vue 3 的 diff 算法会跳过静态子树的比较,只对动态节点进行更新。这减少了不必要的比较操作,提高了性能。
- Vue 2 的 diff 算法对于列表渲染(v-for)时的元素重新排序会比较低效,需要通过给每个元素设置唯一的 key 来提高性能。而 Vue 3 的 diff 算法在列表渲染时,通过跟踪元素的移动,可以更好地处理元素的重新排序,无需设置 key。
- Vue 3 的 diff 算法对于静态节点的处理更加高效,静态节点只会在首次渲染时被处理,后续更新时会直接跳过比较和更新操作,减少了不必要的计算。
总体而言,Vue 3 的 diff 算法在性能方面有所改进,更加高效。它通过一些策略和优化技巧,减少了不必要的比较和计算操作,提高了组件更新的效率。
四、diff分为五种对比策略
源码在packages/runtime-core/src/renderer.ts的patchKeyedChildren()函数。
代码是从上至下执行,五个模块循环都会执行
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)) { // 根据vnode的key和type进行判断
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(n1,n2,container,null,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized)
} else {
break
}
e1--
e2--
}
3、新节点长度 > 旧节点长度
面对这种改变:
a,b,c -> d,a,b,c
此时经历上面两循环,
i=0; l2=4; e1=3; e2=4;
i=0; l2=4; e1=2; e2=3;
i=0; l2=4; e1=1; e2=2;
i=0; l2=4; e1=0; e2=1;
i=0; l2=4; e1=-1 e2=0;
进入循环判断
nextPos=1
anchor=c2[1] // 找到了锚点,要插入的位置就是锚点之前
执行完patch之后,i=1,不满足条件,跳出循环
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
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、乱序
最长递增子序列:在一个数值序列中,找到一个子序列,使得子序列元素的数值依次递增,且长度是最大的
在diff中的作用:减少移动次数
举个🌰:
1,2,3,4,5 -> 1,3,2,5,4 最长递增子序列为:1,2,5所以需要移动5-3=2次。
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key: 创建一个new-vNode array 的一个对应对象,使用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 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
let j
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
// used to track whether any node has moved
let maxNewIndexSoFar = 0
// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
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) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// key-less node, try to locate a key-less node of the same type
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 move and mount
// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap) // 求最长递增子序列的下标
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
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 new
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}