v-for使用注意事项
v-for和v-if是不能一同使用
例如:我们向通过某一个属性,来动态的决定该元素是否渲染时,我们要使用v-if进行判断,但是v-if的优先级高于v-for,不能实现我们想要的效果。
我们可以通过template来解决这个问题。
html
<ul>
<template v-for="item in arr" :key="item.name">
<li v-if="item.isshow">{{item.name}}</li>
</template>
</ul>
arr: [
{name: "a", isshow: true},
{name: "b", isshow: false},
{name: "c", isshow: true},
{name: "d", isshow: true},
],
key关键字的深度解析
举例:有a,b,c,d一个数组,我们对它进行v-for遍历渲染,然后我们在b后面插入一个新的值f。数组变为a,b,f,c,d;通过ul列表进行渲染,分析有key和无key的情况。
vue源码中通过判断是否有key,来执行不同的对比函数。
无key
没有key时,会执行patchUnkeyedChildren方法。
有key
有key时,会执行patchkeyedChildren方法。
第五步,通过key来建立map,寻找相同的vnode.
- 从不相同的节点位置开始,遍历新的vdom树,建立keyToNewIndexMap ,key -> i
- 从不相同的节点位置开始,遍历旧的vdom树,通过旧vnode的key从keyToNewIndexMap找到要插入到新vdom树的位置的索引 i 。如果没有找到索引,则卸载当前的旧vnode。如果找到了i ,则将当前旧vnode和 新vdom树中索引为i的vnod进行patch,更新dom。
- 如果没有找到对应的旧node,就挂载新的vnode
js
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
//遍历新的节点树,简历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
//遍历旧的vnode树
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
// 如果旧vnode有key,则从map中找到应该插到新vnode中的index
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
}
}
}
// newIndex === undefined ,说明在旧vnode树种没有找到与新vnode相同的vnode
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
//找到了相同vnode,让这两个相同类型的vnode进行patch
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
// 如果没有找到对应的旧node,就挂载新的vnode
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--
}
}
}
}
上面代码片段用于高效更新列表(如 v-for
生成的循环元素)的核心 diff 算法(双端比较 + 最长递增子序列优化)。以下是其替换循环元素的关键步骤分析:
1. 建立新节点的 Key-Index 映射表
javascript
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = c2[i] // 新子节点
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i) // 记录 key 对应的新索引
}
}
- 目的:快速通过
key
查找新节点位置,避免后续遍历。
2. 遍历旧节点,寻找可复用的节点
javascript
// newIndexToOldIndexMa表示新节点在旧节点中的索引(0 表示新节点无对应旧节点)。
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
for (i = s1; i <= e1; i++) {
const prevChild = c1[i] // 旧子节点
if (patched >= toBePatched) {
unmount(prevChild) // 多余的旧节点直接卸载
continue
}
// 通过 key 或遍历查找新节点位置
let newIndex = prevChild.key != null
? keyToNewIndexMap.get(prevChild.key)
: findIndexByType(prevChild, c2, s2, e2)
if (newIndex === undefined) {
unmount(prevChild) // 无对应新节点,卸载
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1 // 记录新旧索引关系
patch(prevChild, c2[newIndex]) // 更新节点内容
patched++
}
}
- 核心操作:
- 复用相同
key
或同类型节点,通过patch()
更新内容。 - 无复用的旧节点被卸载。
- 记录新旧索引映射到
newIndexToOldIndexMap
(0 表示新节点无对应旧节点)。
- 复用相同
3. 计算最长递增子序列(LIS)
javascript
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: []
- 目的:LIS 代表无需移动的节点,其相对顺序在新旧列表中一致。
- 优化依据:移动不在 LIS 中的节点,最小化 DOM 操作。
4. 移动或挂载新节点
javascript
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex]
if (newIndexToOldIndexMap[i] === 0) {
// 全新节点,挂载
patch(null, nextChild, container, anchor)
} else if (moved) {
// 需要移动的节点
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor)
} else {
j-- // LIS 中的节点保持不动
}
}
}
- 关键逻辑:
- 无对应旧节点时,挂载新节点。
- 移动不在 LIS 中的节点到正确位置。
- LIS 中的节点已处于正确位置,无需移动。
总结:替换循环元素的策略
- 复用:通过
key
或类型匹配复用旧节点,更新内容。 - 卸载:无对应新节点的旧节点被移除。
- 挂载:新增节点插入到正确位置。
- 移动优化:通过 LIS 减少不必要的 DOM 移动。
这种算法以 O(n)
复杂度高效处理列表更新,优先复用节点,避免大规模 DOM 操作,是 Vue 高效渲染的核心机制之一。