深入淺出: 談下Vue 中的 key 值

key 的一個錯誤使用——使用 index 作爲 key
不知道你在寫 v-for 的時候,會不會直接使用 index 作爲它的 key 值,是的,我承認我會,不得不說,這真的不是一個好習慣。

根據上篇文章,我們還是用 sortable.js 作爲例子討論。以下是核心代碼,其中 arrData 的值爲 [1,2,3,4]

<div id="sort">
  <div v-for="(item,index) in arrData" :key="index" >
    <div>{{item}}</div>
  </div>
</div>


 

 

 mounted () {
    let el = document.getElementById('sort')
    var sortable = new Sortable(el, {
      onEnd: (e) => {
        const tempItem = this.arrData.splice(e.oldIndex, 1)[0]
        this.arrData.splice(e.newIndex, 0, tempItem)
      }
    })
  }


當然一開始的時候,數據渲染肯定是沒有問題的

好了,我們來看下以下的操作:

 

可以看到,我將3拖到2上面的時候,下面的數據變成了 1342,但是上面視圖的還是1234。然後我第四位置拖到第三位置的時候,下面的數據也是生效的,但是上面的數據似乎全部錯亂了。很好,我們重現了案發現場。

接着我改了綁定的 key 值,因爲這裏的例子比較特殊,我們就認爲 item 的值都不相同

<div id="sort">
  <div v-for="(item,index) in arrData" :key="item" >
    <div>{{item}}</div>
  </div>
</div>


再看效果:

是的,這個時候數據就完全跟視圖同步了。

爲什麼?

先看官方文檔中 key 的一句介紹

有相同父元素的子元素必須有獨特的 key。重複的 key 會造成渲染錯誤。

之所以會造成上面渲染錯誤的情況,是因爲我們的 key 值不是獨特的,比如上面的 key 值,在調整數組順序後就每一項原來的 key 值都變了,所以導致了渲染錯誤。

我們先來得出一個結論,用 index 作爲 key 值是有隱患的,除非你能保證 index 始終能夠能夠作爲一個唯一的標識

key 值到底有什麼用
在 vue2.0 之後,我們不寫 key 的話,就會報 warning,那也就是說官方是希望我們寫 key 值的,那麼 key 到底在 vue 中扮演了什麼樣的角色?

不適用 key 可以提高性能麼
答案是,是的!可以!

先看官方解釋:

如果不使用 key,Vue 會使用一種最大限度減少動態元素並且儘可能的嘗試修復/再利用相同類型元素的算法。使用 key,它會基於 key 的變化重新排列元素順序,並且會移除 key 不存在的元素。

比如現在有一個數組 [1,2,3,4]變成了[2,1,3,4],那麼沒有 key 的值會採取一種“就地更新策略”,見下圖。它不會移動元素節點的位置,而是直接修改元素本身,這樣就節省了一部分性能

而對於有 key 值的元素,它的更新方式如下圖所示。可以看到,這裏它對 DOM 是移除/添加的操作,這是比較耗性能的。

竟然不帶 key 性能更優,爲何還要帶 key
先來看一個例子,核心代碼如下,這裏模仿一個切換 tab 的功能,也就是切換的tab1 是1,2,3,4。tab2 是 5,6,7,8。其中有設置了一個點擊設置第一項字體色爲紅色的功能。

那麼當我們點擊tab1將字體色設置成紅色之後,再切換到 tab2,我們預期的結果是我們第一項字體的初始顏色而不是紅色,但是結果卻還是紅色。

<div id="sort">
  <button @click="trunToTab1">tab1</button>
  <button @click="trunToTab2">tab2</button>
  <div v-for="(item, index) in arrData">
    <div @click="clickItem(index)" class="item">{{item}}</div>
  </div>
</div>


 

   

   trunToTab1 () {
        this.arrData = [1,2,3,4]
      },
      trunToTab2 () {
        this.arrData = [5,6,7,8]
      },
      clickItem () {
        document.getElementsByClassName('item')[0].style.color = 'red'
      }


這就超出了我們的預期了,也就是官方文檔所說的,默認模式指的就是不帶 key 的狀態,對於依賴於子組件狀態或者臨時 DOM 狀態的,這種模式是不適用的。

這個默認的模式是高效的,但是隻適用於不依賴子組件狀態或臨時 DOM 狀態 (例如:表單輸入值) 的列表渲染輸出。

我們來看帶上 key 之後的效果

這就是官方文檔之所以推薦我們寫 key 的原因,根據文檔的介紹,如下:

使用 key,它會基於 key 的變化重新排列元素順序,並且會移除 key 不存在的元素。
它也可以用於強制替換元素/組件而不是重複使用它。當你遇到如下場景的時候它可能會很有用:

完整地觸發組件的生命週期鉤子
觸發過渡
那麼 Vue 底層 key 值到底是怎麼去做到以上的功能?我們就得聊聊 diff 算法以及虛擬 DOM 了。

key 在 diff 算法中的作用
這裏我們不談 diff 算法的具體,只看 key 值在其中的作用。(diff 算法有機會我們再聊)

看 vue 源碼中 ``

if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)


我們整理一下代碼塊:

  // 如果有帶 key
  if (isUndef(oldKeyToIdx)) {
    // 創建 index 表
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
  }
  if (isDef(newStartVnode.key)) {
    // 有 key ,直接從上面創建中獲取
    idxInOld = oldKeyToIdx[newStartVnode.key]
  } else {
    // 沒有key, 調用 findIdxInOld
    idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
  }
那麼最主要還是 createKeyToOldIdx 和 findIdxInOld 兩個函數的比較,那麼他們做了什麼呢?

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}


 function findIdxInOld (node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
      const c = oldCh[i]
      if (isDef(c) && sameVnode(node, c)) return i
    }
  }


我們可以看到,如果我們有 key 值,我們就可以直接在 createKeyToOldIdx 方法中創建的 map 對象中根據我們的 key 值,直接找到相應的值。沒有 key 值,則需要遍歷才能拿到。相比於遍歷,映射的速度會更快。

key 值是每一個 vnode 的唯一標識,依靠 key,我們可以更快的拿到 oldVnode 中相對應的節點。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章