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

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

作者头像
奋飛
发布于 2023-07-24 06:41:03
发布于 2023-07-24 06:41:03
30400
代码可运行
举报
文章被收录于专栏:Super 前端Super 前端
运行总次数:0
代码可运行

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
/* 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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
<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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
前端二面vue面试题总结_2023-03-01
在 Vue3.0 中变量必须 return 出来, template 中才能使用;而在 Vue3.2 中只需要在 script 标签上加上 setup 属性,无需 return, template 便可直接使用,非常的香啊!
用户10377014
2023/03/01
8050
DIff算法看不懂就一起来锤我(带图)
面试官:"你了解虚拟DOM(Virtual DOM)跟Diff算法吗,请描述一下它们";
coder_koala
2021/09/18
7910
DIff算法看不懂就一起来锤我(带图)
vue面试常见考察点总结
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
bb_xiaxia1998
2022/10/13
8640
前端常见vue面试题合集
通过webpack的tree-shaking功能,可以将无用模块“剪辑”,仅打包需要的
bb_xiaxia1998
2022/11/09
7390
理解DOM Diff算法
虚拟 DOM 出现的背景:在 jQuery 时代,可以自行控制 DOM 操作的时机,手动调整,但是当项目很大时,操作 DOM 的复杂度就会上来,DOM 操作会很耗费性能,操作 DOM 就还需要考虑优化 DOM 操作,提升性能。《高性能 JavaScript》这本书中说,把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。操作 DOM 后需要经过跨流程通信和渲染线程触发的重新渲染(重绘或者重排),在开发中,应尽量减少操作 DOM。而虚拟 DOM 出现后,更新 DOM 交给框架处理。操作虚拟 DOM 可能并没有操作真实 DOM 快,但是它让开发人员不再把很多精力放在操作 DOM 上,而是专注于处理业务数据。本文以 Vue 原码中的 DOM diff 算法为例,介绍一下这个算法的实现原理。
多云转晴
2020/07/29
1.1K0
理解DOM Diff算法
那些年曾经没回答上来的vue面试题
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
bb_xiaxia1998
2022/09/26
5310
面试官:了解过vue中的diff算法吗?说说看
diff 算法的在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
@超人
2021/02/26
7610
面试官:了解过vue中的diff算法吗?说说看
「源码级回答」大厂高频Vue面试题(中)
本篇是「源码级回答」大厂高频Vue面试题系列的第二篇,本篇也是选择了面试中经常会问到的一些经典面试题,从源码角度去分析。
前端森林
2020/05/21
9960
「源码级回答」大厂高频Vue面试题(中)
Vue2剥丝抽茧-虚拟 dom 之增删
虚拟 dom 之移动优化 中介绍了虚拟 dom 的双端 diff 的算法,但是没有考虑当虚拟 dom 增加或者减少的情况,这篇文章介绍增删 dom 在各个场景下的的代码完善。
windliang
2022/08/20
2420
Vue2剥丝抽茧-虚拟 dom 之增删
Vue2剥丝抽茧-虚拟 dom 之移动优化
虚拟 dom 之移动 中我们介绍了一个简单的虚拟 dom diff 的算法,这篇文章主要介绍一下对它的优化。
windliang
2022/08/20
4240
Vue2剥丝抽茧-虚拟 dom 之移动优化
VUE中diff比较
要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。
全栈程序员站长
2021/07/01
7030
Vue中diff算法的理解
diff算法用来计算出Virtual DOM中改变的部分,然后针对该部分进行DOM操作,而不用重新渲染整个页面,渲染整个DOM结构的过程中开销是很大的,需要浏览器对DOM结构进行重绘与回流,而diff算法能够使得操作过程中只更新修改的那部分DOM结构而不更新整个DOM,这样能够最小化操作DOM结构,能够最大程度上减少浏览器重绘与回流的规模。
WindRunnerMax
2020/08/27
7070
Vue 2 常见面试题速查
当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty 将它们转为 getter / setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
Cellinlab
2023/05/17
1.2K0
Vue 2 常见面试题速查
3. 「snabbdom@3.5.1 源码分析」patch(如何打补丁?)
看到会返回一个patch函数。看到init内部有很多函数,这些函数大都都是用到api进行DOM操作,而api依赖入参domApi(如果放在外侧,domApi需要作为参数传递)。 这里实际上通过闭包私有化这些函数作为方法存在。
tinyant
2023/02/24
1.7K0
浅析 Snabbdom 中 vnode 和 diff 算法
目前前端使用最多的就是 vue 或 react 了,我们在学习这两个框架的过程中,总有一个绕不开的话题:vnode,也就是虚拟 DOM。什么是虚拟 DOM,引用一段 vue 官方的解释就是:
政采云前端团队
2022/03/29
7520
浅析 Snabbdom 中 vnode 和 diff 算法
​vue源码分析前置知识必备
最近利用空闲时间又翻看了一遍Vue的源码,只不过这次不同的是看了Flow版本的源码。说来惭愧,最早看的第一遍时对Flow不了解,因此阅读的是打包之后的vue文件,大家可以想象这过程的痛苦,没有类型的支持,看代码时摸索了很长时间,所以我们这次对Vue源码的剖析是Flow版本的源码,也就是从Github上下载下来的源码中src目录下的代码。不过,在分析之前,我想先说说阅读Vue源码所需要的一些知识点,掌握这些知识点之后,相信再阅读源码会较为轻松。
前端老鸟
2019/07/29
6400
图文解析vue2.0的diff算法
vue2.0加入了virtual dom,有向react靠拢的意思。vue的diff位于patch.js文件中,我的一个小框架aoy也同样使用此算法,该算法来源于snabbdom,复杂度为O(n)。 了解diff过程可以让我们更高效的使用框架。 本文力求以图文并茂的方式来讲明这个diff的过程。
前端迷
2019/08/15
7130
「源码剖析」如何实现一个虚拟DOM算法
上篇文章《虚拟DOM如何进化为真实DOM》中讲到了如何通过虚拟DOM树转化为真实DOM渲染到页面中。但是在渲染的过程中,我们直接将新的虚拟DOM树转化成真实DOM替换掉旧的DOM结构。当真实的DOM中的状态或者内容发生变化的时候,重新渲染新的虚拟DOM树再替换掉旧的,这样的话会显得很无力。
小丑同学
2021/02/07
6690
Diff算法核心原理
一直是我写文章的宗旨,今天我就用通俗的方式来讲解一下Diff算法吧?Lets Go
ruochen
2021/11/21
6230
Vue进阶 Diff算法详解
虚拟DOM就是把真实DOM树的结构和信息抽象出来,以对象的形式模拟树形结构,如下:
前端LeBron
2021/12/08
6240
Vue进阶 Diff算法详解
相关推荐
前端二面vue面试题总结_2023-03-01
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验