面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

  • 面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)已关闭评论
  • 121 次浏览
  • A+
所属分类:Web前端
摘要

在vue中使用v-for时需要,都会提示或要求使用  :key,有的的开发者会直接使用数组的 index 作为 key 的值,但不建议直接使用 index作为 key 的值,有时我们面试时也会遇到面试官问:为什么不推荐使用 index 作为 key ?接下来和小颖一起来瞅瞅吧


前言

在vue中使用v-for时需要,都会提示或要求使用  :key,有的的开发者会直接使用数组的 index 作为 key 的值,但不建议直接使用 index作为 key 的值,有时我们面试时也会遇到面试官问:为什么不推荐使用 index 作为 key ?接下来和小颖一起来瞅瞅吧

为什么要有 key

官网解释

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by="$index"

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

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key attribute:

key的作用

官网解释:key

预期:number | string | boolean (2.4.2 新增) | symbol (2.5.12 新增)

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

通俗解释:

key 在 diff 算法的作用,就是用来判断是否是同一个节点。

Vue 中使用虚拟 dom 且根据 diff 算法进行新旧 DOM 对比,从而更新真实 dom ,key 是虚拟 DOM 对象的唯一标识, 在 diff 算法中 key 起着极其重要的作用,key可以管理可复用的元素,减少不必要的元素的重新渲染,也要让必要的元素能够重新渲染。

为什么key 值不建议用index?

性能消耗

使用 index 做 key,破坏顺序操作的时候, 因为每一个节点都找不到对应的 key,导致部分节点不能复用,所有的新 vnode 都需要重新创建。

示例:

<template>   <div class="hello">     <ul>       <li v-for="(item,index) in studentList" :key="index">{{ item.name }}</li>     </ul>     <br>     <button @click="addStudent">添加一条数据</button>   </div> </template>  <script> export default {   name: 'ceshi',   data() {     return {       studentList: [         {id: 1, name: '张三', age: 18},         {id: 2, name: '李四', age: 19},         {id: 3, name: '王麻子', age: 20},       ],     };   },   methods: {     addStudent() {       const studentObj = {id: 4, name: '王五', age: 20};       this.studentList = [studentObj, ...this.studentList]     }   } } </script>

打开浏览器的开发工具,修改数据的文本,后面加上 “-我没变”,点击添加一条数据   按钮,则发现dom整体都变了

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 

 面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 当给key绑定唯一不重复的值时:

<li v-for="item in studentList" :key="item.id">{{ item.name }}</li>

打开浏览器的开发工具,修改数据的文本,后面加上 “-我没变”,点击添加一条数据   按钮,则发现只是顶部多了一条,其他dom没有重新渲染。

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 

 面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

当用index做key时,当我们在前面加了一条数据时 index 顺序就会被打断,导致新节点 key 全部都改变了,所以导致我们页面上的数据都被重新渲染了。而用了不会重复的唯一标识id时,diff算法比较后发现只有头部有变化,其他没有变,则只给头部新增了一个元素。从上面比较可以看出,用唯一值作为 key 可以节约开销这样大家应该就明白了吧·················

参考:在 Vue 中为什么不推荐用 index 做 key

数据错位

示例:

<template>   <div class="hello">     <ul>       <li v-for="item in studentList" :key="item.id">{{ item.name }}<input /></li>     </ul>     <br>     <button @click="addStudent">添加一条数据</button>   </div> </template>  <script> export default {   name: 'ceshi',   data() {     return {       studentList: [         {id: 1, name: '张三', age: 18},         {id: 2, name: '李四', age: 19}       ],     };   },   methods: {     addStudent() {       const studentObj = {id: 4, name: '王五', age: 20};       this.studentList = [studentObj, ...this.studentList]     }   } } </script>

我们往 input 里面输入一些值,添加一位同学看下效果:

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 这时候我们就会发现,在添加之前输入的数据错位了。添加之后王五的输入框残留着张三的信息,这很显然不是我们想要的结果。

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 

 

 从上面比对可以看出来这时因为采用 index 作为 key 时,当在比较时,发现虽然文本值变了,但是当继续向下比较时发现  DOM 节点还是和原来一摸一样,就复用了,但是没想到 input 输入框残留输入的值,这时候就会出现输入的值出现错位的情况

当我们将key绑定为唯一标识id时,如图所示。key 相同的节点都做到了复用。起到了diff 算法的真正作用。

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 

 总结:

  • 用 index 作为 key 时,在对数据进行,逆序添加,逆序删除等破坏顺序的操作时,会产生没必要的真实 DOM更新,从而导致效率低
  • 用 index 作为 key 时,如果结构中包含输入类的 DOM,会产生错误的 DOM 更新
  • 在开发中最好每条数据使用唯一标识固定的数据作为 key,比如后台返回的 ID,手机号,身份证号等唯一值
  • 如果不对数据进行逆序添加 逆序删除破坏顺序的操作, 只用于列表展示的话 使用index 作为Key没有毛病

相关内容

 为什么要提出虚拟DOM

在Web早期,页面的交互比较简单,没有复杂的状态需要管理,也不太需要频繁的操作DOM,随着时代的发展,页面上的功能越来越多,我们需要实现的需求也越来越复杂,DOM的操作也越来越频繁。通过js操作DOM的代价很高,因为会引起页面的重排重绘,增加浏览器的性能开销,降低页面渲染速度,既然操作DOM的代价很高那么有没有那种方式可以减少对DOM的操作?这就是为什么提出虚拟DOM一个很重要的原因。
 
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。

参考: VUE中key的作用与diff算法

 

简而言之呢   

      频繁的操作DOM会影响浏览器的性能,为了解决这个问题从而提出了 虚拟DOM,虚拟DOM是用javascript对象表示的,而操作javascript是很简便高效的。虚拟DOM和真正的DOM有一层映射关系,很多需要操作DOM的地方都会去操作虚拟DOM,最后统一一次更新DOM。 因而可以提高性能。

附加面试题:

如果有许多次操作dom的动作,vue中是怎么更新dom的?
vue中不会立即操作dom,而是将要更新的diff内容保存到新的虚拟dom对象中,通过diff算法后得到最终的虚拟dom,将其映射成真是的dom更新视图。

虚拟DOM是什么

      Vue.js通过编译将模版转换成渲染函数(render),执行渲染函数就可以得到一个以vnode节点(JavaScript对象)作为基础的树形结构,vnode节点里面包含标签名(tag)、属性(attrs)和子元素对象(children)等等属性,这个树形结构就是Virtual DOM,简单来说,可以把Virtual DOM理解为一个树形结构的JS对象。

参考: 浅谈Vue中的虚拟DOM

简而言之  虚拟DON就是javascript对象,通过对虚拟 DOM进行diff,算出最小差异,然后更新真实的DOM。

虚拟DOM的优势

  1. 提供一种方便的工具,使得开发效率得到保证
  2. 保证最小化的DOM操作,使得执行效率得到保证
  3. 具备跨平台的优势 由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
  4. 提升渲染性能 Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新

参考:vue 中虚拟dom的理解浅谈Vue中的虚拟DOM

为什么虚拟DOM可以提高渲染速度

  1. 根据虚拟dom树最初渲染成真实dom
  2. 当数据变化,或者说是页面需要重新渲染的时候,会重新生成一个新的完整的虚拟dom
  3. 拿新的虚拟dom来和旧的虚拟dom做对比(使用diff算法)。得到需要更新的地方之后,从而只对发生了变化的节点进行更新操作。

如果大家不太明白的话,我们来看个示例

<template>   <div class="hello">     <ul>       <li v-for="(item,index) in studentList" :key="item.id" @click="changeName(index)">{{ item.name }}</li>     </ul>   </div> </template>  <script> export default {   name: 'ceshi',   data() {     return {       studentList: [         {id: 1, name: '张三', age: 18},         {id: 2, name: '李四', age: 19},         {id: 3, name: '王麻子', age: 20},       ],     };   },   methods: {     changeName(index) {       this.studentList[index].name = '我变啦'     }   } } </script>

上面的修改数组值得方式 仅在 2.2.0+ 版本中支持 Array + index 用法。

如果使用之前的则使用    Vue.set( target, propertyName/index, value )

运行后,我们打开开发者工具,然后手动修改页面的文本,给每个后面加  “-我没变” 

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 然后随便点击其中一个,我们会发现只有点击那个文本变了,并且只是它的文本内容变了,dom并没有整体变。

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

vue中是如何实现模板转换成视图的

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

参考: 浅谈Vue中的虚拟DOM

简单来说就是:通过编译将模版转换成渲染函数(render),执行渲染函数就可以得到一个以vnode节点(JavaScript对象)作为基础的树形结构(虚拟dom),当数据发生变化虚拟dm通过diff算法找出新树和旧树的不同,记录两棵树差异根据差异应用到所构建的真正的DOM树上,视图就更新。

diff算法

 虚拟DOM中,在DOM的状态发生变化时,虚拟DOM会进行Diff运算,来更新只需要被替换的DOM,而不是全部重绘。 在Diff算法中,只平层的比较前后两棵DOM树的节点,没有进行深度的遍历。

规则

同层比较,如上面div的Old Vnode,跟其Vnode比较,div只会跟同层div比较,不会跟p进行比较,下面是示例图:

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

流程图

当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。

面试题——为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

 

 具体分析

patch

 

 来看看patch是怎么打补丁的(代码只保留核心部分)

function patch (oldVnode, vnode) {     // some code     if (sameVnode(oldVnode, vnode)) {         patchVnode(oldVnode, vnode)     } else {         const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点         let parentEle = api.parentNode(oEl)  // 父元素         createEle(vnode)  // 根据Vnode生成新元素         if (parentEle !== null) {             api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素             api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点             oldVnode = null         }     }     // some code      return vnode }

patch函数接收两个参数oldVnodeVnode分别代表新的节点和之前的旧节点

  • 判断两节点是否值得比较,值得比较则执行patchVnode
    function sameVnode (a, b) {   return (     a.key === b.key &&  // key值     a.tag === b.tag &&  // 标签名     a.isComment === b.isComment &&  // 是否为注释节点     // 是否都定义了data,data包含一些具体信息,例如onclick , style     isDef(a.data) === isDef(b.data) &&       sameInputType(a, b) // 当标签是<input>的时候,type必须相同   ) }

  • 不值得比较则用Vnode替换oldVnode

如果两个节点都是一样的,那么就深入检查他们的子节点。如果两个节点不一样那就说明Vnode完全被改变了,就可以直接替换oldVnode

虽然这两个节点不一样但是他们的子节点一样怎么办?别忘了,diff可是逐层比较的,如果第一层不一样那么就不会继续深入比较第二层了。(我在想这算是一个缺点吗?相同子节点不能重复利用了...)

patchVnode

当我们确定两个节点值得比较之后我们会对两个节点指定patchVnode方法。那么这个方法做了什么呢?

patchVnode (oldVnode, vnode) {     const el = vnode.el = oldVnode.el     let i, oldCh = oldVnode.children, ch = vnode.children     if (oldVnode === vnode) return     if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {         api.setTextContent(el, vnode.text)     }else {         updateEle(el, vnode, oldVnode)         if (oldCh && ch && oldCh !== ch) {             updateChildren(el, oldCh, ch)         }else if (ch){             createEle(vnode) //create el's children dom         }else if (oldCh){             api.removeChildren(el)         }     } }

这个函数做了以下事情:

  • 找到对应的真实dom,称为el
  • 判断VnodeoldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
  • 如果oldVnode有子节点而Vnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
  • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要

参考:详解vue的diff算法 

updataChildren原理