v-for 值范围〡key〡diff
一、v-for 的值范围
v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n
的取值范围重复多次,举个例子:
<span v-for="n in 10" :key="n">
{{ n }}
</span>
<!-- 相当于 -->
<span v-for="(item, index) in Array(10).fill('temp')" :key="index">
{{ item + 1 }}
</span>
注意此处 n
的初值是从 1 开始而非 0。
二、v-for 中的 key 值
1、key 值的作用
在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,尽可能地就地更新或复用相同类型的元素。
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
与 React Key 同理,Vue 中 v-for 需要使用 key 来给每个节点设置一个唯一标识,使 diff 算法可以正确识别此节点,找到正确的位置插入新的节点,从而高效的更新虚拟 DOM。
注意
key 值不能相同,而且不能使用对象或数组之类的非基本类型作为 key 的值。
另外,不建议使用索引来用作 key 值,因为列表项目的顺序可能发生变化,使用索引作 key 值会导致性能变差或出现 bug。
2、不与 v-for 结合的用法
除了在 v-for 中使用 key 之外,还可以单独在元素上使用,这么做是为了在 key 值发生变化时强制替换元素,而非对它进行更新复用,当需要重新触发生命周期和触发过渡时很有用,举个例子:
<transition>
<span :key="text">{{ text }}</span>
</transition>
这里当 text
变化时,<span>
会被替换而非更新复用,因此 transition
将会被触发。
三、Vue diff 原理
虚拟 DOM 是用 JS 模拟 DOM 结构,避免 DOM 树的频繁更新,从而提升页面构建性能,而 diff 算法就是用来高效地对比新旧虚拟 DOM,找出真实 DOM 变化之处。Vue diff 原理如下:
当数据发生改变时,set
方法会调用 Dep.notify 通知所有订阅者 Watcher,订阅者就会调用 patch
给真实的 DOM 打补丁,更新相应的视图。patch
主要做了以下判断:
- 如果没有新节点,则直接执行旧节点的
destroy
钩子函数; - 如果没有旧节点,则说明页面刚开始初始化,调用
createElm
; - 如果旧节点和新节点一样,则调用
patchVnode
去处理这两个节点; - 如果旧节点和新节点不一样,则创建新节点,删除旧节点。
src/core/vdom/patch.ts
// https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts#L801
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
// 没有新节点时直接执行 destroy 钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
// 没有旧节点时直接用新节点生成 dom 元素
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点是否和新节点自身一样,一样则执行 patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则直接销毁旧节点,根据新节点生成 dom 元素
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (__DEV__) {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
总结:
当数据发生变化时,订阅者 Watcher 会调用 patch
函给真实 DOM 打补丁,通过新旧节点的比较,对节点进行处理,更新相应的视图。