Skip to main content

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 主要做了以下判断:

  1. 如果没有新节点,则直接执行旧节点的 destroy 钩子函数;
  2. 如果没有旧节点,则说明页面刚开始初始化,调用 createElm
  3. 如果旧节点和新节点一样,则调用 patchVnode 去处理这两个节点;
  4. 如果旧节点和新节点不一样,则创建新节点,删除旧节点。
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 打补丁,通过新旧节点的比较,对节点进行处理,更新相应的视图。

点击查看 React diff 原理