在使用v-for进行列表渲染时,通常会给元素或者组件绑定一个key属性。
-
官方的解释:
key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;
如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法;
而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;
1. 插入F的案例
- 这个案例是当我点击按钮时会在中间插入一个f
<template id="my-app">
<ul>
<li v-for="item in letters" :key="item">{{item}}</li>
</ul>
<button @click="insertF">插入元素</button>
</template>
<SCript src="../js/vue.js"></SCript>
<SCript>
const App = {
template:'#my-app',
data() {
return {
letters: ['a','b','c','d']
}
},
methods: {
insertF() {
this.letters.splice(2, 0, 'f')
}
},
}
Vue.createApp(App).mount('#app')
</SCript>
-
可以确定的是,这次更新对于ul和button是不需要进行更新的,需要更新的是li列表
在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列表
因为对于列表中a、b、c、d他们都是没有变化的
在操作真实DOM的时候,我们只需要在中间插入一个f的li即可
-
那么Vue中对于列表的更新究竟是如何操作的呢?
Vue事实上会对于有key和没有key会调用两个不同的方法
有key,那么就使用 patchKeyedChildren 方法
没有key,那么就使用 patchUnkeyedChildren 方法
2. Vue源码对于key的判断
3. 没有key的操作及过程
没有key的操作(Vue源码)
const patchUnkeyedChildren = (
c1: VNode[], //旧的nodes['a','b','c','d']
c2: VNodeArrayChildren, //新的nodes['a','b','f','c','d']
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
//1.获取旧节点的长度
const oldLength = c1.length
//2.获取新节点的长度
const newLength = c2.length
//3.获取最小的那个长度 取最小的是为了数组不会越界
const commonLength = Math.min(oldLength, newLength)
let i
//4.从0的位置开始依次patch比较
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
)
}
//5.如果旧的节点数大于新的节点数
if (oldLength > newLength) {
// remove old
//移除旧的剩余的节点
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength
)
} else {
// mount new
//创建新的节点
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
commonLength
)
}
}
没有key的过程
-
会发现上面的diff算法效率并不高:
c和d来说它们事实上并不需要有任何改动
但是因为c被f所用,所以后续所有的内容都要进行一次改动,并且最后进行新增
4. 有key的操作及过程
有key的操作(Vue源码)
// can be all-keyed or mixed
const 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.从头开始遍历,遇到相同的节点就继续,遇到不同的就跳出循环
// 1. sync from start
// (a b) c
// (a b) d e
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(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
//节点不同就直接跳出循环
break
}
i++
}
// 2.从尾部开始遍历,遇到相同的节点就继续,遇到不同的就跳出循环
// 2. sync from end
// a (b c)
// d e (b c)
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.如果最后新节点更多,那么就添加新节点
// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
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, //为null的时候就表示是一次挂载
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
// 4.如果旧节点更多,那么就移除旧节点
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. 如果中间存在不知道如何排列的位置序列,那么就使用key建立索引图,最大限度的使用旧节点
// 如果是未知的节点序列
// 如果有多余的节点,那么就移除节点
// 之后是移动节点和挂载新节点
// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key:index map for newChildren
// 根据key建立map索引图
const keyToNewIndexMap: Map<string | number, 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--
}
}
}
}
}
有key的diff算法过程
-
第一步的操作是从头开始进行遍历、比较:
a和b是一致的就会继续进行比较;
c和f因为key不一致,所以会break跳出循环;
- 第二步的操作是从尾部开始进行遍历、比较:
-
第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:
-
第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:
-
第五步是最特色的情况,中间还有很多未知的或者乱序的节点:
-
所以可以发现,Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作:
在没有key的时候我们的效率是非常低的;
在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效;