前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【揭秘Vue核心】为什么不建议在 v-for 指令中使用 index 作为 key,让你秒懂!

【揭秘Vue核心】为什么不建议在 v-for 指令中使用 index 作为 key,让你秒懂!

作者头像
奋飛
发布2023-07-24 14:41:03
2700
发布2023-07-24 14:41:03
举报
文章被收录于专栏:Super 前端

问题:为什么不建议在 v-for 指令中使用 index 作为 key?

代码语言:javascript
复制
<div v-for="(item, index) in items" :key="index">
  <!-- 内容 -->
</div>

key 的必要性

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

为了避免上述情况,可以为每个元素对应的块提供一个唯一的 key attribute。

这个特殊的 key attribute 主要作为 Vue 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode

这里提到了两个内容:vnode(虚拟DOM)和 比较新旧节点。

先写下总结:

1. vnode(虚拟 DOM )是为了避免频繁操作真实 DOM 带来的性能损耗;

2. 比较新旧节点(diff 算法)是在 patch 子 vnode 过程中,找到与新 vnode 对应的老 vnode,复用真实的dom节点,避免不必要的性能开销。

总之,目的就是减少真实DOM的操作,提升性能。

vnode(虚拟DOM)

与其说虚拟 DOM 是一种具体的技术,不如说是一种模式,所以并没有一个标准的实现。这个概念是由 React 率先开拓,随后被许多不同的框架采用,当然也包括 Vue。 – 源自 vue 官网

vnode简单示例:

代码语言:javascript
复制
const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* 更多 vnode */
  ]
}

vnode 是一个纯 JavaScript 的对象 (一个“虚拟节点”),它代表着一个 <div> 元素。它包含我们创建实际元素所需的所有信息。它还包含更多的子节点,这使它成为虚拟 DOM 树的根节点。

这里,有必要先提下整个构建流程(以vue举例)

过程

说明

Template => render function code

编译

render function code => Virtual DOM tree => Actual DOM

挂载

  1. 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
  2. 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
  3. 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

这里我们清楚了,vnode 是作为渲染函数与真实DOM的桥梁! 虚拟 DOM 带来的主要收益是它让开发者能够灵活、声明式地创建、检查和组合所需 UI 的结构,同时只需把具体的 DOM 操作留给渲染器去处理。

而上面提到的比较新旧节点(diff 算法),就是在发生更新过程中,如何对新旧两份虚拟DOM进行比较的过程,遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。

diff 算法

篇幅有限,无法详尽的说明 diff 的具体机制,只针对自己的理解,做简单梳理,目的是为了说明开头抛出的「为什么不建议在 v-for 指令中使用 index 作为 key」。如果需要了解 diff 算法细则,大家可自行查阅。

示例: old vnode:[A B C D E F G H] new vnode:[A B D E C I G H]

判断是否为相同节点,这里使用到了 key。

sameVnode:https://github.com/vuejs/vue/blob/HEAD/src/core/vdom/patch.ts#L36-L37

代码语言:javascript
复制
function sameVnode(a, b) {
  return (
    a.key === b.key &&   // 如果key不相同,会被认定为不是相同的节点
    a.asyncFactory === b.asyncFactory &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
  )
}

patch:https://github.com/vuejs/vue/blob/HEAD/src/core/vdom/patch.ts#L801-L802

代码语言:javascript
复制
function patch(oldVnode, vnode, hydrating, removeOnly) {  
  if (sameVnode(oldVnode, vnode)) {
    // patch existing root node
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  } else {
    // replacing existing element
    const oldElm = oldVnode.elm		// 当前oldVnode对应的真实元素节点
    const parentElm = nodeOps.parentNode(oldElm) // / 父元素
    createElm(vnode) 	// 创建新元素
    insert(parentElm, vnode.elm, refElm) // 在createElm中实现
    if (isDef(parentElm)) {
      removeVnodes([oldVnode], 0, 0)  // 移除以前的旧元素节点
    }
  }
  return vnode.elm
}

patchVnode:https://github.com/vuejs/vue/blob/HEAD/src/core/vdom/patch.ts#L584-L585

代码语言:javascript
复制
function patchVnode(oldVnode, vnode) {
  if (oldVnode === vnode) {
    return	// 同一个对象,直接return
  }
  const elm = (vnode.elm = oldVnode.elm)
  let i
  const data = vnode.data
  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isUndef(vnode.text)) {	
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) // oldVnode 和 vnode children 都有子节点
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) { 
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) { 
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
}

updateChildren:https://github.com/vuejs/vue/blob/HEAD/src/core/vdom/patch.ts#L413-L414

代码语言:javascript
复制
/* newStartIdx、newEndIdx:new vnode 第一个和最后一个节点index值
 * oldStartIdx、oldEndIdx:old vnode 第一个和最后一个节点index值 */
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
   if (sameVnode(oldStartVnode, newStartVnode)) {		 // 下述「第1步」从头开始patch
     patchVnode(...)
     oldStartVnode = oldCh[++oldStartIdx]
     newStartVnode = newCh[++newStartIdx]
   } else if (sameVnode(oldEndVnode, newEndVnode)) { // 下述「第2步」从尾开始patch
     patchVnode(...)
     oldEndVnode = oldCh[--oldEndIdx]
     newEndVnode = newCh[--newEndIdx]
   } else if (sameVnode(oldStartVnode, newEndVnode)) { // 下述「第3步」
     // Vnode moved right
     patchVnode(...)
     canMove && nodeOps.insertBefore(...)
     oldStartVnode = oldCh[++oldStartIdx]
     newEndVnode = newCh[--newEndIdx]
   } else if (sameVnode(oldEndVnode, newStartVnode)) { // 下述「第4步」
     // Vnode moved left
     patchVnode(...)
     canMove && nodeOps.insertBefore(...)
     oldEndVnode = oldCh[--oldEndIdx]
     newStartVnode = newCh[++newStartIdx]
   } else {
     // 这一部分比较重要,截图源码说明
   }
}
  1. old vnode 头与new vnode 头对比,diff patch,直到第一个不相同节点(C/D)结束;
  2. old vnode 尾与new vnode 尾对比,diff patch,直到第一个不相同节点(F/I)结束;
  3. old vnode 头与new vnode 尾对比,diff patch,直到第一个不相同节点(C/I)结束;
  4. old vnode 尾与new vnode 头对比,diff patch,直到第一个不相同节点(F/D)结束;
  5. 经过头尾遍历后,会有三种结果: 【情况1】如果 old vnode 全部patch完成,new vnode 还没完成,则创建新增的节点; => 结束 【情况2】如果 new vnode 全部patch完成,old vnode 还没完成,则删除多余的节点;=> 结束 【情况3】如果 new vnode 和 old vnode 都还有剩余节点;=> 需要继续执行,示例情况
  6. 针对【情况3】,剩余节点处理 ① 遍历 old vnode 剩余节点,存入到 createKeyToOldIdx<key, index> => {C:2, D:3, E:4, F:5} ② 针对 剩余的 new vnode 节点:
    • 如果存在key,则通过 createKeyToOldIdx 索引是否存在;
    • 如果不存在key,则遍历剩余 oldCh,获取index;=> findIdxInOld
  7. 判断是否索引到 index 【情况1】没有索引到,说明无法复用老的,直接新建; 【情况2】索引到了,如果是相同的节点,直接移动; 【情况3】索引到了,只是key相同,但节点发生了变化,直接新建;

​ ------- 至此,直到上述循环结束,oldStartIdx > oldEndIdx || newStartIdx > newEndIdx -------

代码语言:javascript
复制
if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(...)
} else if (newStartIdx > newEndIdx) {
	removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
  1. 如果 oldStartIdx > oldEndIdx,剩余新节点无法找到可复用内容,直接新建;
  2. 如果newStartIdx > newEndIdx,新节点已执行完成,剩余的老节点无意义,直接删除。

总结

在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,并尽可能地就地更新/复用相同类型的元素。如果传了 key,则将根据 key 的变化顺序来重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。

代码语言:javascript
复制
<script setup>
import { ref } from 'vue'

const list = ref([
  { name: '项目1' },
  { name: '项目2'},
  { name: '项目3'}
])
function del(index) {
  list = list.value.splice(index, 1)
}
</script>

<template>
 <div>
		<div v-for="(item,index) in list" :key="index">
			<span>{{item.name}}</span>
      <input />
			<button @click="del(index)">删除</button>
		</div>
	</div>
</template>

使用 index 作为key, 当点击删除第二条数据时,可以看到文本框的内容还是原本的第二条数据的内容。原因是虚拟DOM在比较元素的时候,因为DOM上的key等属性均未发生变化,所以其自身和内部的input均被复用了。

所以,在实际开发过程中不要把 index 作为 key 值。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • key 的必要性
  • vnode(虚拟DOM)
  • diff 算法
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档