key在Vue列表渲染时究竟起到了什么作用

本文深入探讨Vue中v-for指令结合key属性的作用,解析其如何优化DOM更新过程,提高列表渲染效率,尤其是在复杂列表场景下。同时,文章强调了在不同场景下选择合适key值的重要性,以及对性能的影响。

Vue2+采用diff算法来进行新旧vnode的对比从而更新DOM节点。而通常在我们使用v-for这个指令的时候,Vue会要求你给循环列表的每一项添加唯一的key,那么这个key在渲染列表时究竟起到了什么作用呢?

在解释这一点之前,你最好已经了解Vuediff算法的具体原理是什么。

Vue2更新真实DOM的操作主要是两种:创建新DOM节点并移除旧DOM节点更新已存在的DOM节点,这两种方式里创建新DOM节点的开销肯定是远大于更新或移动已有的DOM节点,所以在diff中逻辑都是为了减少新的创建而更多的去复用已有DOM节点来完成DOM的更新。

在新旧vnodediff过程中,key是判断两个节点是否为同一节点的首要条件:


// 参见Vue2源码 core/vdom/patch.js

function sameVnode (a, b) {
    return (
        a.key === b.key && (
            (
                a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b)
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                a.asyncFactory === b.asyncFactory &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

复制代码

值得注意的是,如果新旧vnodekey值都未定义的话那么两个key都为undefineda.key === b.key 是成立的

接下来是在updateChildren方法中,这个方法会对新旧vnode进行diff,然后将比对出的结果用来更新真实的DOM


// 参见Vue2源码 core/vdom/patch.js

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    ...
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            ...
        } else if (isUndef(oldEndVnode)) {
            ...
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            ...
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            ...
        } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            ...
        } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            ...
        } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
                : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { // New element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                    oldCh[idxInOld] = undefined
                    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // same key but different element. treat as new element
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                }
            }
            newStartVnode = newCh[++newStartIdx]
        }
    }
    ...
}
复制代码

设置key的可以在diff中更快速的找到对应节点,提高diff速度

updateChildren方法的while循环中,如果头尾交叉对比没有结果,即oldStartVnode存在且oldEndVnode存在且新旧children首尾四个vnode互不相同的条件下,会根据newStartVnodekey去对比oldCh数组中的key,从而找到相应oldVnode

首先通过createKeyToOldIdx方法创建一个关于oldChmap

if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

function createKeyToOldIdx (children, beginIdx, endIdx) {
    let i, key
    const map = {}
    for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}
复制代码

这个map中将所有定义了keyoldVnode在数组中的index值作为键值,它的key作为键名存储起来,然后赋给oldKeyToIdx

idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

function findIdxInOld (node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
        const c = oldCh[i]
        if (isDef(c) && sameVnode(node, c)) return i
    }
}
复制代码

如果newStartVnodekey存在的话,就去oldKeyToIdx中寻找相同key所对应的index值,这样就能拿到跟newStartVnodekey相同的oldVnodeoldCh数组中的index,即得到了与newStartVnode对应的oldVnode。如果找不到的话,那么idxInOld就为undefined

而如果newStartVnode并没有设置key,则通过findIdxInOld方法遍历oldCh来获取与newStartVnode互为sameVnodeoldVnode,返回这个oldVnodeoldCh数组的index。(前面介绍过,Vue在更新真实DOM时倾向于真实DOM节点的复用,所以在这里还是会选择去找对应的oldVnode,来更新已有的DOM节点)

这时候设置key的好处就显而易见了,有key存在时我们可以通过map映射快速定位到对应的oldVnode然后进行patch,没有key值时我们需要遍历这个oldCh数组然后去一一进行比较,相比之下肯定是key存在时diff更高效。

接下来就是更新DOM的过程,如果oldCh[idxInOld]存在且与newStartVnode互为sameVnode存在则先更新再移动,否则创建新的element

if (isUndef(idxInOld)) { // New element
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
    vnodeToMove = oldCh[idxInOld]
    if (sameVnode(vnodeToMove, newStartVnode)) {
        patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldCh[idxInOld] = undefined
        canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    } else {
        // same key but different element. treat as new element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    }
}
复制代码

那么设置key值就一定能提高diff效率吗?

答案是否定的


`<div v-for="i in arr">{{ i }}</div>`

// 如果我们的数组是这样的
[1, 2, 3, 4, 5]

// 它的渲染结果是这样的
`<div>1</div>`  // key: undefined
`<div>2</div>`  // key: undefined
`<div>3</div>`  // key: undefined
`<div>4</div>`  // key: undefined
`<div>5</div>`  // key: undefined

// 将它打乱
[4, 1, 3, 5, 2]

// 渲染结果是这样的 期间只发生了DOM节点的文本内容的更新
`<div>4</div>`  // key: undefined
`<div>1</div>`  // key: undefined
`<div>3</div>`  // key: undefined
`<div>5</div>`  // key: undefined
`<div>2</div>`  // key: undefined


// 如果我们给这个数组每一项都设置了唯一的key
[{id: 'A', value: 1}, {id: 'B', value: 2}, {id: 'C', value: 3}, {id: 'D', value: 4}, {id: 'E', value: 5}]

// 它的渲染结果应该是这样的
`<div>1</div>`  // key: A
`<div>2</div>`  // key: B
`<div>3</div>`  // key: C
`<div>4</div>`  // key: D
`<div>5</div>`  // key: E

// 将它打乱
[{id: 'D', value: 4}, {id: 'A', value: 1}, {id: 'C', value: 3}, {id: 'E', value: 5}, {id: 'B', value: 2}]

// 渲染结果是这样的  期间只发生了DOM节点的移动
`<div>4</div>`  // key: D
`<div>1</div>`  // key: A
`<div>3</div>`  // key: C
`<div>5</div>`  // key: E
`<div>2</div>`  // key: B
复制代码

我们给数组设置了key之后数组的diff效率真的变高了吗?

并没有,因为在简单模板的数组渲染中,新旧节点的key都为undefined,根据sameVnode的判断条件,这些新旧节点的keytag等属性全部相同,所以在sameVnode(oldStartVnode, newStartVnode)这一步的时候就已经判定为对应的节点(不再执行头尾交叉对比),然后直接进行patchVnode,根本没有走后面的那些else。每一次循环新旧节点都是相对应的,只需要更新其内的文本内容就可以完成DOM更新,这种原地复用的效率无疑是最高的。

而当我们设置了key之后,则会根据头尾交叉对比结果去执行下面的if else,进行判断之后还需要执行insertBefore等方法移动真实DOM的节点的位置或者进行DOM节点的添加和删除,这样的查找复用开销肯定要比不带key直接原地复用的开销要高。

Vue文档中对此也进行了说明:

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。

建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

所以,简单列表的渲染可以不使用key或者用数组的index作为key(效果等同于不带key),这种模式下性能最高,但是并不能准确的更新列表项的状态。一旦你需要保存列表项的状态,那么就需要用使用唯一的key用来准确的定位每一个列表项以及复用其自身的状态,而大部分情况下列表组件都有自己的状态。

总结

key在列表渲染中的作用是:在复杂的列表渲染中快速准确的找到与newVnode相对应的oldVnode,提升diff效率

转载于:https://juejin.im/post/5cb98b926fb9a0688e0677f9

### Vue 中 v-for 指令中 key 属性的作用及原理 #### 一、Key 的基本作用Vue.js 中,`v-for` 是一种常用的指令,用于基于一组数据动态渲染多个节点或组件。为了提升性能并优化更新操作,Vue 使用了一种称为 **虚拟 DOM (Virtual DOM)** 的机制[^1]。当数据发生变化Vue 不会直接修改真实 DOM,而是先通过 diff 算法比较新旧虚拟 DOM 树之间的差异,然后再针对这些差异最小化地更新实际 DOM。 在此过程中,`key` 属性起到了至关重要的作用——它帮助 Vue 准确识别哪些元素被添加、删除或者移动了。具体来说: - `key` 提供了一个唯一的标识符给每个节点,使得 Vue 可以更高效地区分不同的子树。 - 如果没有提供 `key` 或者提供了重复的 `key` 值,可能会导致意外的行为,比如状态丢失或不必要的重新渲染[^2]。 #### 二、Diff 算法中的 Key 应用 在 Diff 过程中,Vue 主要依赖于以下几个原则来进行高效的更新操作: 1. **首选判断 key 是否相等**:如果两个节点具有相同的 `key`,则认为它们是同一个节点,从而保留其状态而不必销毁重建; 2. **其次比较其他属性和内容的变化**:只有在确认两者确实不同之后才会触发相应的 DOM 更新动作[^3]。 这种策略极大地减少了无谓的操作次数,提高了应用的整体响应速度。 #### 三、Index vs ID 作为 Key 的区别 虽然可以用数组索引 (`index`) 来充当临性的键值,但这并不是推荐的做法,原因如下: - 当列表项发生增删改查等情况(尤其是涉及位置调整),仅依靠索引来定位容易造成混乱,因为此原有的映射关系可能已经失效。 因此,在大多数情况下建议优先选用能够稳定反映实体身份的信息字段(如数据库记录ID)作为真正的 `key` 参数传递进去,这样无论怎样变动都能保持良好的一致性表现效果更好也更加安全可靠。 #### 四、未指定 Key 的后果 如果没有显式定义任何形式的关键字标记,默认行为将是采用当前项目的下标序号当作替代方案。然而这种方法存在诸多局限性和潜在风险点需要注意规避以免引起不可预见的问题出现: - 数据结构频繁变化可能导致视图层面上产生错乱现象; - 组件内部维护的状态无法正确同步到对应的实例上; 所以最好始终记得为每一个参与迭代展示流程的对象赋予独一无二且持久不变的身份标签即所谓的“key”。 ```javascript // 正确示例 - 推荐做法 <template> <ul> <li v-for="item in items" :key="item.id">{{ item.name }}</li> <!-- 使用稳定的id --> </ul> </template> <script> export default { data() { return { items: [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ] }; } }; </script> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值