首頁  >  文章  >  web前端  >  深入分析 Vue 中不建議用 index 做 key的原因

深入分析 Vue 中不建議用 index 做 key的原因

青灯夜游
青灯夜游轉載
2021-11-29 19:34:571741瀏覽

為什麼在 Vue 不建議用 index 做 key?下面這篇文章就來跟大家分析一下原因,希望對大家有幫助,快來收藏吧!

深入分析 Vue 中不建議用 index 做 key的原因

前端開發中,只要涉及到清單渲染,那麼無論是React 還是Vue 框架,都會提示或要求每個清單項目使用唯一的key,那很多開發者就會直接使用陣列的index 作為key 的值,而並不知道key 的原理。那麼這篇文章就會解釋 key 的作用以及為什麼最好不要使用 index 作為 key 的屬性值。

key 的作用

Vue 中使用虛擬dom 且根據diff 演算法進行新舊DOM 對比,從而更新真實dom ,key 是虛擬DOM 物件的唯一標識, 在diff 演算法中key 起著極為重要的作用。 【相關推薦:《vue.js教學》】

key 在diff 演算法中的角色

其實在React,Vue,中diff 演算法大致是差不多,但是diff 比對方式還是有較大差異的,甚至每個版本diff 都大有不同。下面我們就以Vue3.0 diff 演算法為切入點,剖析key 在diff 演算法中的作用

具體diff 流程如下

深入分析 Vue 中不建議用 index 做 key的原因

##Vue3.0中在patchChildren 方法中有這麼一段原始碼

if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) { 
         /* 对于存在 key 的情况用于 diff 算法 */
        patchKeyedChildren(
         ...
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
         /* 对于不存在 key 的情况,直接 patch  */
        patchUnkeyedChildren( 
          ...
        )
        return
      }
    }

patchChildren 根據是否存在key 進行真正的diff 或直接patch。對於 key 不存在的情況我們就不做深入研究了。

我們先來看看一些宣告的變數。

/*  c1 老的 vnode c2 新的vnode  */
let i = 0              /* 记录索引 */
const l2 = c2.length   /* 新 vnode的数量 */
let e1 = c1.length - 1 /* 老 vnode 最后一个节点的索引 */
let e2 = l2 - 1        /* 新节点最后一个节点的索引 */

同步頭部節點

第一步的事情就是從頭開始尋找相同的vnode,然後進行patch ,如果發現不是相同的節點,那麼立即跳出循環。

//(a b) c
//(a b) d e
/* 从头对比找到有相同的节点 patch ,发现不同,立即跳出*/
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
        /* 判断 key ,type 是否相等 */
      if (isSameVNodeType(n1, n2)) {
        patch(
          ...
        )
      } else {
        break
      }
      i++
    }

流程如下:

深入分析 Vue 中不建議用 index 做 key的原因

isSameVNodeType 作用就是判斷目前vnode 類型和vnode 的key 是否相等

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  return n1.type === n2.type && n1.key === n2.key
}

其實看到這,是不是已經知道key 在diff 演算法的作用,就是用來判斷是否是同一個節點。

同步尾部節點

第二步從尾開始同前diff

//a (b c)
//d e (b c)
/* 如果第一步没有 patch 完,立即,从后往前开始 patch  如果发现不同立即跳出循环 */
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      if (isSameVNodeType(n1, n2)) {
        patch(
         ...
        )
      } else {
        break
      }
      e1--
      e2--
    }

經歷第一步操作之後,如果發現沒有patch 完,那麼立即進行第二步,從尾部開始遍歷依序向前diff。如果發現不是相同的節點,那麼立即跳出循環。 流程如下:

深入分析 Vue 中不建議用 index 做 key的原因

新增新的節點

第三個步驟如果舊節點是否全部patch,新節點沒有被patch 完,建立新的vnode

//(a b)
//(a b) c
//i = 2, e1 = 1, e2 = 2
//(a b)
//c (a b)
//i = 0, e1 = -1, e2 = 0
/* 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的 vnode 处理(这种情况说明已经 patch 完相同的 vnode ) */
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch( /* 创建新的节点*/
            ...
          )
          i++
        }
      }
    }

流程如下:

深入分析 Vue 中不建議用 index 做 key的原因

#刪除多餘節點

第四步如果新節點全部被patch,舊節點有剩餘,那麼卸載所有舊節點

//i > e2
//(a b) c
//(a b)
//i = 2, e1 = 2, e2 = 1
//a (b c)
//(b c)
//i = 0, e1 = 0, e2 = -1
else if (i > e2) {
   while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
   }
}

流程如下:

深入分析 Vue 中不建議用 index 做 key的原因

##最長遞增子序列

到了這一步,比較核心的場景還沒出現,如果運氣好,可能到這裡就結束了,那我們也不能全靠運氣。剩下的一個場景是新舊節點都還有多個子節點存在的情況。那接下來再看看,Vue3 是怎麼做的。為了結合move、新增和卸載的操作

深入分析 Vue 中不建議用 index 做 key的原因每次在對元素進行移動的時候,我們可以發現一個規律,如果想要移動的次數最少,就意味著需要有一部分元素是穩定不動的,那麼究竟能夠保持穩定不動的元素有一些什麼規律呢?

可以看一下上面這個例子:c  h  d  e  VS  d  e  i  c,比對的時候,以肉眼可以看出只需要將c 移動到最後,然後卸載hh,新增i 就好了。 d  e 可以保持不動,可以發現 d  e 在新舊節點中的順序都是不變的,d 在 e 的後面,下標處於遞增狀態。

这里引入一个概念,叫最长递增子序列。
官方解释:在一个给定的数组中,找到一组递增的数值,并且长度尽可能的大。
有点比较难理解,那来看具体例子:

const arr = [10, 9, 2, 5, 3, 7, 101, 18]
=> [2, 3, 7, 18]
这一列数组就是arr的最长递增子序列,其实[2, 3, 7, 101]也是。
所以最长递增子序列符合三个要求:
1、子序列内的数值是递增的
2、子序列内数值的下标在原数组中是递增的
3、这个子序列是能够找到的最长的
但是我们一般会找到数值较小的那一组数列,因为他们可以增长的空间会更多。

那接下来的思路是:如果能找到老节点在新节点序列中顺序不变的节点们,就知道,哪一些节点不需要移动,然后只需要把不在这里的节点插入进来就可以了。**因为最后要呈现出来的顺序是新节点的顺序,移动是只要老节点移动,所以只要老节点保持最长顺序不变,通过移动个别节点,就能够跟它保持一致。**所以在此之前,先把所有节点都找到,再找对应的序列。最后其实要得到的则是这一个数组:[2, 3, 新增 , 0]。其实这就是 diff 移动的思路了

深入分析 Vue 中不建議用 index 做 key的原因

为什么不要用index

性能消耗

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

例子:

<template>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="index">{{item.name}}</li>
      <br>
      <button @click="addStudent">添加一条数据</button>
    </ul>

  </div>
</template>

<script>
export default {
  name: &#39;HelloWorld&#39;,
  data() {
    return {
      studentList: [
        { id: 1, name: &#39;张三&#39;, age: 18 },
        { id: 2, name: &#39;李四&#39;, age: 19 },
      ],
    };
  },
  methods:{
    addStudent(){
      const studentObj = { id: 3, name: &#39;王五&#39;, age: 20 };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</script>

我们先把 Chorme 调试器打开,我们双击把里面文本修改一下

深入分析 Vue 中不建議用 index 做 key的原因

我们运行以上上面的代码,看下运行结果

深入分析 Vue 中不建議用 index 做 key的原因

从上面运行结果可以看出来,我们只是添加了一条数据,但是三条数据都需要重新渲染是不是很惊奇,我明明只是插入了一条数据,怎么三条数据都要重新渲染?而我想要的只是新增的那一条数据新渲染出来就行了。

上面我们也讲过 diif 比较方式,下面根据 diff 比较绘制一张图,看看具体是怎么比较的吧

深入分析 Vue 中不建議用 index 做 key的原因

当我们在前面加了一条数据时 index 顺序就会被打断,导致新节点 key 全部都改变了,所以导致我们页面上的数据都被重新渲染了。

下面我们下面生成1000个 DOM 来比较一下采用 index ,和不采用 index 性能比较,为了保证 key 的唯一性我们采用 uuid 作为 key

我们用 index 做为 key 现执行一遍

<template>
  <div class="hello">
    <ul>
      <button @click="addStudent">添加一条数据</button>
      <br>
      <li v-for="(item,index) in studentList" :key="index">{{item.id}}</li>
    </ul>
  </div>
</template>

<script>
import uuidv1 from &#39;uuid/v1&#39;
export default {
  name: &#39;HelloWorld&#39;,
  data() {
    return {
      studentList: [{id:uuidv1()}],
    };
  },
  created(){
    for (let i = 0; i < 1000; i++) {
      this.studentList.push({
        id: uuidv1(),
      });
    }
  },
  beforeUpdate(){
    console.time(&#39;for&#39;);
  },
  updated(){
    console.timeEnd(&#39;for&#39;)//for: 75.259033203125 ms
  },
  methods:{
    addStudent(){
      const studentObj = { id: uuidv1() };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</script>

换成 id 作为 key

<template>
  <div class="hello">
    <ul>
      <button @click="addStudent">添加一条数据</button>
      <br>
      <li v-for="(item,index) in studentList" :key="item.id">{{item.id}}</li>
    </ul>
  </div>
</template>
  beforeUpdate(){
    console.time(&#39;for&#39;);
  },
  updated(){
    console.timeEnd(&#39;for&#39;)//for: 42.200927734375 ms
  },

从上面比较可以看出,用唯一值作为 key 可以节约开销

数据错位

上述例子可能觉得用 index 做 key 只是影响页面加载的效率,认为少量的数据影响不大,那面下面这种情况,可能用 index 就可能出现一些意想不到的问题了,还是上面的场景,这时我先再每个文本内容后面加一个 input 输入框,并且手动在输入框内填写一些内容,然后通过 button 向前追加一位同学看看

<template>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="index">{{item.name}}<input /></li>
      <br>
      <button @click="addStudent">添加一条数据</button>
    </ul>
  </div>
</template>

<script>
export default {
  name: &#39;HelloWorld&#39;,
  data() {
    return {
      studentList: [
        { id: 1, name: &#39;张三&#39;, age: 18 },
        { id: 2, name: &#39;李四&#39;, age: 19 },
      ],
    };
  },
  methods:{
    addStudent(){
      const studentObj = { id: 3, name: &#39;王五&#39;, age: 20 };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</script>

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

深入分析 Vue 中不建議用 index 做 key的原因

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

1深入分析 Vue 中不建議用 index 做 key的原因

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

解决方案

既然知道用 index 在某些情况下带来很不好的影响,那平时我们在开发当中怎么去解决这种情况呢?其实只要保证 key 唯一不变就行,一般在开发中用的比较多就是下面三种情况。

  • 在开发中最好每条数据使用唯一标识固定的数据作为 key,比如后台返回的 ID,手机号,身份证号等唯一值

  • 可以采用 Symbol 作为 key,Symbol 是 ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。

let a=Symbol(&#39;测试&#39;)
let b=Symbol(&#39;测试&#39;)
console.log(a===b)//false
  • 可以采用 uuid 作为 key ,uuid 是 Universally Unique Identifier 的缩写,它是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符

我们采用上面第一种方案作为 key 在看一下上面情况,如图所示。key 相同的节点都做到了复用。起到了diff 算法的真正作用。

深入分析 Vue 中不建議用 index 做 key的原因深入分析 Vue 中不建議用 index 做 key的原因

1深入分析 Vue 中不建議用 index 做 key的原因

總結

  • #用index 作為key 時,在對資料進行,逆序添加,逆序刪除等破壞順序的操作時,會產生沒必要的真實DOM更新,從而導致效率低
  • 用index 作為key 時,如果結構中包含輸入類別的DOM,會產生錯誤的DOM 更新
  • 在開發中最好每條資料使用唯一標識固定的資料作為key,例如後台傳回的ID,手機號,身分證號等唯一值
  • 如果不存在對資料逆序添加,逆序刪除等破壞順序的操作時,僅用於渲染展示用時,使用index 作為key 也是可以的(但是還是不建議使用,養成良好開發習慣)。

原文網址:https://juejin.cn/post/7026119446162997261

作者:政採雲前端團隊

更多程式相關知識,請造訪:程式設計入門! !

以上是深入分析 Vue 中不建議用 index 做 key的原因的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除