Vue模板中使用v-for
指令時不建議將index
作為:key
屬性莉给。今天我在看項目代碼時,發(fā)現(xiàn)有多年開發(fā)經(jīng)驗的前端老鳥也犯這樣的低級錯誤问裕。
今天我們就從其原理上說明為什么不建議將 index
作為 v-for
的 key
逮壁,除非你能確定該 v-for
遍歷的數(shù)組長度始終不會發(fā)生變化,不過在這個需求多變的時代誰能保證產(chǎn)品不會想一出是一出呢粮宛?
當我們在模板里使用了響應(yīng)式變量時窥淆,當變化值發(fā)生變化時,其對應(yīng)的dom對象也會觸發(fā)更新巍杈,這背后就是vue內(nèi)部的 vdom diff
算法過程
如下模板代碼
<ul>
<li>a</li>
<li>b</li>
</ul>
生成的 VDOM
大致如下
{
tag: 'ul',
children: [
{ tag: 'li', children: [ { vnode: { text: 'a' }}] },
{ tag: 'li', children: [ { vnode: { text: 'b' }}] },
]
}
之后把兩個li順序改變下
{
tag: 'ul',
children: [
{ tag: 'li', children: [ { vnode: { text: 'b' }}] },
{ tag: 'li', children: [ { vnode: { text: 'a' }}] },
]
}
接下來就是diff算法發(fā)揮作用了忧饭,首先就是響應(yīng)式 更新之后,會調(diào)用渲染 Watcher 的回調(diào)函數(shù)vm._update(vm._render())去驅(qū)動視圖更新筷畦,vm._render() 其實生成的就是 vnode词裤,而 vm._update 就會帶著新的 vnode 去走觸發(fā) patch 過程刺洒。
- 其中首先判斷的就是節(jié)點是否相同:
- 不相同:就會直接刪除舊的vnode,渲染新的vnode吼砂;
- 相同:就要讓節(jié)點盡可能多的復(fù)用逆航;
但是節(jié)點相同就要判斷很多情況,如:vnode是文字的話就直接替換掉文字渔肩;vnode不是的話就要對children進行比較(ul中l(wèi)i做比較)因俐;如果有新vnode,沒有舊的vnode周偎,就需要增加節(jié)點抹剩;如果沒有新vnode,有舊vnode蓉坎,那么就要刪除節(jié)點澳眷;如果新舊節(jié)點都有,就需要diff算法了袍嬉;
// 舊首節(jié)點
let oldStartIdx = 0
// 新首節(jié)點
let newStartIdx = 0
// 舊尾節(jié)點
let oldEndIdx = oldCh.length - 1
// 新尾節(jié)點
let newEndIdx = newCh.length - 1
這是用變量把新節(jié)點首尾境蔼,舊節(jié)點的首尾表示灶平,在while中伺通,不斷的對新舊節(jié)點進行比較,直到指針收縮到?jīng)]有節(jié)點可以比較逢享。
其中有一個函數(shù) sameVnode罐监,
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
)
)
}
它是用來判斷節(jié)點是否可用的關(guān)鍵函數(shù),可以看到瞒爬,判斷是否是 sameVnode弓柱,傳遞給節(jié)點的 key 是關(guān)鍵。
然后我們接著進入 diff 過程侧但,每一輪都是同樣的對比矢空,其中某一項命中了,就遞歸的進入 patchVnode 針對單個 vnode 進行的過程(如果這個 vnode 又有 children禀横,那么還會來到這個 diff children 的過程 ):
- 舊首節(jié)點和新首節(jié)點用sameNode 對比屁药;
- 舊尾節(jié)點和新尾節(jié)點用sameNode 對比;
- 舊首節(jié)點和新尾節(jié)點用sameNode 對比柏锄;
- 舊尾節(jié)點和新首節(jié)點用sameNode 對比酿箭;
- 如果以上邏輯都匹配不到,再把所有舊子節(jié)點的 key 做一個映射到舊節(jié)點下標的 key -> index 表趾娃,然后用新 vnode 的 key 去找出在舊節(jié)點中可以復(fù)用的位置缭嫡。
然后不停的把匹配到的指針向內(nèi)部收縮,直到新舊節(jié)點有一端的指針相遇(說明這個端的節(jié)點都被patch過了)抬闷。
在指針相遇以后妇蛀,還有兩種比較特殊的情況:
有新節(jié)點需要加入。 如果更新完以后,oldStartIdx > oldEndIdx讥耗,說明舊節(jié)點都被 patch 完了有勾,但是有可能還有新的節(jié)點沒有被處理到。接著會去判斷是否要新增子節(jié)點古程。
有舊節(jié)點需要刪除蔼卡。 如果新節(jié)點先patch完了,那么此時會走 newStartIdx > newEndIdx 的邏輯挣磨,那么就會去刪除多余的舊子節(jié)點雇逞。
我們可以使用 Vue SFC Playground 來演示這個過程
<script setup>
import { ref } from 'vue'
const list= ref(['111', '222', '333', '444'])
const handleAddd = () => {
list.value.unshift('test' + Math.random().toString(16).slice(2))
}
const handleRemove = index => {
list.value.splice(index, 1)
}
</script>
<template>
<ul>
<li v-for="(item, index) in list" :key="index">
item: {{item}}
<button @click="handleRemove(index)">
刪除
</button>
</li>
</ul>
<button @click="handleAddd">
新增
</button>
</template>
如上代碼所示,我們使用index
作為key
時茁裙,此時我們在數(shù)組 list
的頭部添加一個元素塘砸,會導(dǎo)致其他li
進行不必要的更新。
刪除也是如此晤锥,由于li的key發(fā)生變化掉蔬,會導(dǎo)致不必要的更新
此時我們將 key
綁定為 item
時,將只更新需要更新的dom
應(yīng)確保綁定的
key
在list
中能唯一矾瘾,不與其他項相同