1. 程式人生 > 程式設計 >Vue 中為什麼不推薦用index 做 key屬性值

Vue 中為什麼不推薦用index 做 key屬性值

目錄
  • 前言
  • key 的作用
    • key 在 diff 演算法中的角色
      • 同步頭部節點
      • 同步尾部節點
      • 新增新的節點
      • 刪除多餘節點
      • 最長遞增子序列
  • 為什麼不要用 index
    • 效能消耗
      • 資料錯位
      • 解決方案
        • 總結

          前言

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

          key 的作用

          Vue 中使用虛擬 dom 且根據 diff 演算法進行新舊 DOM 對比,從而更新真實 dom ,key 是虛擬 DOM 物件的唯一標識,在 diff 演算法中 key 起著極其重要的作用。

          key 在 diff 演算法中的角色

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

          具體 diff 流程如下

          圖片

          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++
              }

          流程如下:

          圖片

          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。如果發現不是相同的節點,那麼立即跳出迴圈。流程如下:

          新增新的節點

          第三步如果老節點是否全部 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++
                  }
                }
              }

          流程如下:

          圖片

          刪除多餘節點

          第四步如果新節點全部被 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++
             }
          }

          流程如下:

          最長遞增子序列

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

          Vue 中為什麼不推薦用index 做 key屬性值

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

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

          這裡引入一個概念,叫最長遞增子序列。
          官方解釋:在一個給定的陣列中,找到一組遞增的數值,並且長度儘可能的大。
          有點比較難理解,那來看具體例子:

          const arr = [10,9,2,5,3,7,101,18]
          => [2,18]
          這一列陣列就是arr的最長遞增子序列,其實[2,101]也是。
          所以最長遞增子序列符合三個要求:
          1、子序列內的數值是遞增的
          2、子序列內數值的下標在原陣列中是遞增的
          3、這個子序列是能夠找到的最長的
          但是我們一般會找到數值較小的那一組數列,因為他們可以增長的空間會更多。

          那接下來的思路是:如果能找到老節點在新節點序列中順序不變的節點們,就知道,哪一些節點不需要移動,然後只需要把不在這裡的節點插入進來就可以了。因為最後要呈現出來的順序是新節點的順序,移動是隻要老節點移動,所以只要老節點保持最長順序不變,通過移動個別節點,就能夠跟它保持一致。所以在此之前,先把所有節點都找到,再找對應的序列。最後其實要得到的則是這一個陣列:[2,新增,0]。其實這就是 diff 移動的思路了

          圖片

          為什麼不要用 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: 'HelloWorld',data() {
              return {
                studentList: [
                  { id: 1,name: '張三',age: 18 },{ id: 2,name: '李四',age: 19 },],};
            },methods:{
              addStudent(){
                const studentObj = { id: 3,name: '王五',age: 20 };
                this.studentList=[studentObj,...this.studentList]
              }
            }
          }
          <www.cppcns.com/script>

          我們先把 Chorme 偵錯程式開啟,我們雙擊把裡面文字修改一下

          圖片

          我們執行以上上面的程式碼,看下執行結果

          圖片

          從上面執行結果可以看出來,我們只是添加了一條資料,但是三條資料都需要重新渲染是不是很驚奇,我明明只是插入了一條資料,怎麼三條資料都要重新渲染?而我想要的只是新增的那一條資料新渲染出來就行了。

          上面我們也講過 diff 比較方式,下面根據 diff 比較繪製一張圖,看看具體是怎麼比較的吧

          圖片

          當我們在前面加了一條資料時 index 順序就會被打斷,導致新節點 key 全部都改變了,所以導致我們頁面上的資料都被重新渲染了。

          下面我們下面生成 1000 個 DOM 來比較一下采用 index ,和不採用 index 效能比較,為了保證 key 的唯一性我們採用 uuid 作為 key

          我們用 index 做為 key 先執行一遍

          <template>
            <div class="hello">
              <ul>
                <button @click="addStudent">新增一條資料</button>
                <br>
              http://www.cppcns.com  <li v-for="www.cppcns.com(item,index) in studentList" :key="index">{{item.id}}</li>
              </ul>
            </div>
          </template>
           
          <script>
          import uuidv1 from 'uuid/v1'
          export default {
            name: 'HelloWorld',data() {
              return {
                studentList: [{id:uuidv1()}],created(){
              for (let i = 0; i < 1000; i++) {
                this.studentList.push({
                  id: uuidv1(),});
              }
            },beforeUpdate(){
              console.time('for');
            },updated(){
              console.timeEnd('for')//for: 75.259033203125 ms
            },methods:{
              addStudent(){
                const studentObj = { id: uuidv1() };
                this.studentList=[studentObj,...this.studentList]
              }
            }
          }
          </script>

          換成 id 作為 key

          <template>
            <div class="hello">
              &http://www.cppcns.comlt;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('for');
            },updated(){
              console.timeEnd('for')//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: 'HelloWorld',...this.studentList]
              }
            }
          }
          </script>

          我們往 input 裡面輸入一些值,新增一位同學看下效果:

          圖片

          這時候我們就會發現,在新增之前輸入的資料錯位了。新增之後王五的輸入框殘留著張三的資訊,這很顯然不是我們想要的結果。

          圖片

          從上面比對可以看出來這時因為採用 index 作為 key 時,當在比較時,發現雖然文字值變了,但是當繼續向下比較時發現 Input DOM 節點還是和原來一摸一樣,就複用了,但是沒想到 input 輸入框殘留輸入的值,這時候就會出現輸入的值出現錯位的情況

          解決方案

          既然知道用 index 在某些情況下帶來很不好的影響,那平時我們在開發當中怎麼去解決這種情況呢?其實只要保證 key 唯一不變就行,一般在開發中用的比較多就是下面三種情況。

          • 在開發中最好每條資料使用唯一標識固定的資料作為 key,比如後臺返回的 ID,手機號,身份證號等唯一值
          • 可以採用 Symbol 作為 key,Symbol 是 ES6 引入了一種新的原始資料型別 Symbol ,表示獨一無二的值,最大的用法是用來定義物件的唯一屬性名。
          let a=Symbol('測試')
          let b=Symbol('測試')
          console.log(a===b)//false

          可以採用 uuid 作為 key ,uuid 是 Universally Unique Identifier 的縮寫,它是在一定的範圍內(從特定的名字空間到全球)唯一的機器生成的識別符號

          我們採用上面第一種方案作為 key 再看一下上面情況,如圖所示。key 相同的節點都做到了複用。起到了diff 演算法的真正作用。

          圖片

          圖片

          圖片

          總結

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

          到此這篇關於在 Vue 中為什麼不推薦用 index 做 key的文章就介紹到這了,更多相關Vue index 做 key內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!