寫在前面
此係列來源於開源項目:前端 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
節點。
- 更準確:因爲帶 key 就不是
就地複用
了,在sameNode
函數a.key === b.key
對比中可以避免就地複用的情況。所以會更加準確。 - 更快:利用 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 的原因。