【前端100問】Q1:寫 React/Vue 項目時爲什麼要在列表組件中寫 key, 其作用是什麼?

寫在前面

此係列來源於開源項目:前端 100 問:能搞懂 80%的請把簡歷給我
爲了備戰 2021 春招
每天一題,督促自己
從多方面多角度總結答案,豐富知識
問題原文鏈接

正文回答

基於沒有 key 的情況 diff 速度會更快。

沒有綁定 key 的情況下,並且在遍歷模板簡單的情況下,會導致虛擬新舊節點對比更快,節點也會複用。
而這種複用是就地複用,一種鴨子辯型的複用。

 vm.dataList = [4, 1, 3, 5, 2] // 數據位置替換

 // 沒有key的情況, 節點位置不變,但是節點innerText內容更新了
  [
    '<div>4</div>', // id: A
    '<div>1</div>', // id:  B
    '<div>3</div>', // id:  C
    '<div>5</div>', // id:  D
    '<div>2</div>'  // id:  E
  ]

  // 有key的情況,dom節點位置進行了交換,但是內容沒有更新
  // <div v-for="i in dataList" :key='i'>{{ i }}</div>
  [
    '<div>4</div>', // id: D
    '<div>1</div>', // id:  A
    '<div>3</div>', // id:  C
    '<div>5</div>', // id:  E
    '<div>2</div>'  // id:  B
  ]
  vm.dataList = [3, 4, 5, 6, 7] // 數據進行增刪

  // 1. 沒有key的情況, 節點位置不變,內容也更新了
  [
    '<div>3</div>', // id: A
    '<div>4</div>', // id:  B
    '<div>5</div>', // id:  C
    '<div>6</div>', // id:  D
    '<div>7</div>'  // id:  E
  ]

  // 2. 有key的情況, 節點刪除了 A, B 節點,新增了 F, G 節點
  // <div v-for="i in dataList" :key='i'>{{ i }}</div>
  [
    '<div>3</div>', // id: C
    '<div>4</div>', // id:  D
    '<div>5</div>', // id:  E
    '<div>6</div>', // id:  F
    '<div>7</div>'  // id:  G
  ]

從以上來看,不帶有 key,並且使用簡單的模板,基於這個前提下,可以更有效的複用節點,diff 速度來看也是不帶 key 更加快速的,因爲帶 key 在增刪節點上有耗時。
這就是 vue 文檔所說的默認模式。但是這個並不是 key 作用,而是沒有 key 的情況下可以對節點就地複用,提高性能。
這種模式會帶來一些隱藏的副作用,比如可能不會產生過渡效果,或者在某些節點有綁定數據(表單)狀態,會出現狀態錯位。
VUE 文檔也說明了 這個默認的模式是高效的,但是隻適用於不依賴子組件狀態或臨時 DOM 狀態 (例如:表單輸入值) 的列表渲染輸出

key 的作用

key 是給每一個vnode的唯一 id,可以依靠key,更準確, 更快的拿到oldVnode中對應的vnode節點。

  1. 更準確:因爲帶 key 就不是就地複用了,在sameNode函數 a.key === b.key對比中可以避免就地複用的情況。所以會更加準確。
  2. 更快:利用 key 的唯一性生成 map 對象來獲取對應節點,比遍歷方式更快。

diff 算法的一些說明

在交叉對比中,當新節點跟舊節點頭尾交叉對比沒有結果時,會根據新節點的 key 去對比舊節點數組中的 key,從而找到相應舊節點(這裏對應的是一個 key => index 的 map 映射)。如果沒找到就認爲是一個新增節點。而如果沒有 key,那麼就會採用遍歷查找的方式去找到對應的舊節點。一種一個 map 映射,另一種是遍歷查找。相比而言。map 映射的速度更快。

// vue項目  src/core/vdom/patch.js  -488行
// 以下是爲了閱讀性進行格式化後的代碼

// oldCh 是一箇舊虛擬節點數組
if (isUndef(oldKeyToIdx)) {
  oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}

if (isDef(newStartVnode.key)) {
  // map 方式獲取
  idxInOld = oldKeyToIdx[newStartVnode.key];
} else {
  // 遍歷方式獲取
  idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
}

// 創建map函數
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;
}

// 遍歷尋找
// sameVnode 是對比新舊節點是否相同的函數
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 的作用是爲了在數據變化時強制更新組件,以避免“原地複用”帶來的副作用。另外,某些情況下不帶 key 可能性能更好
  • 主要是爲了提升 diff【同級比較】的效率。自己想一下自己要實現前後列表的 diff,如果對列表的每一項增加一個 key,即唯一索引,那就可以很清楚的知道兩個列表誰少了誰沒變。而如果不加 key 的話,就只能一個個對比了。
  • 官網推薦的使用 key,應該理解爲“使用唯一 id 作爲 key”。因爲 index 作爲 key,和不帶 key 的效果是一樣的。index 作爲 key 時,每個列表項的 index 在變更前後也是一樣的,都是直接判斷爲 sameVnode 然後複用。

舉一些實際應用

一個新聞列表,可點擊列表項來將其標記爲"已訪問",可通過 tab 切換“娛樂新聞”或是“社會新聞”。

不帶 key 屬性的情況下,在“娛樂新聞”下選中第二項然後切換到“社會新聞”,"社會新聞"裏的第二項也會是被選中的狀態,因爲這裏複用了組件,保留了之前的狀態。

要解決這個問題,可以爲列表項帶上新聞 id 作爲唯一 key,那麼每次渲染列表時都會完全替換所有組件,使其擁有正確狀態。

帶上唯一 key 雖然會增加開銷,但是對於用戶來說基本感受不到差距,而且能保證組件狀態正確,這應該就是爲什麼推薦使用唯一 id 作爲 key 的原因。

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