在列表中使用key已是老生常談的問題了,官方 最佳實踐 也大力推薦使用key。。。不對,官方原話是:javascript
在組件上老是必須用 key 配合 v-for,以便維護內部組件及其子樹的狀態。html
既然官方都說的這麼中肯了,那麼確定是很重要了。可是,當初不明真相的我,老是以爲寫和不寫也沒啥區別啊,反正都能渲染出來。寫着好麻煩啊~🥱,算了,下次,下次必定寫。。。vue
其實之因此看不出來區別是由於一切發生的太快了。。。直到有一天我打了一個斷點才發現區別大的不止一點點,事情是這樣的java
我寫了一個下面這樣的列表,開始有五個元素,分別是A,B,C,D,E
。在組件mount事後兩秒向列表頭部插入一個元素F
,而後對比一下使用key
和不使用key
的區別node
<div id="app">
<ul>
<!-- <li v-for="item in list" :key="item">{{item}}</li>-->
<li v-for="item in list">{{item}}</li>
</ul>
<h2>不使用key,仔細看上面元素的變化</h2>
</div>
複製代碼
const app = new Vue({
data() {
return {
list: ['A', 'B', 'C', 'D', 'E']
}
},
el: '#app',
mounted() {
setTimeout(() => {
this.list.unshift('F')
}, 2000)
}
})
複製代碼
聰明的你可能會問了,爲何呢?git
咱們知道,虛擬dom
是一個樹結構,當組件數據變化時會執行組件的patchVnode
方法,而後按照深度優先,同層比較的原則進行diff
。若是新老節點都有子節點,則調用updateChildren
方法進行子節點的比較,虛擬dom diff
的核心算法就在這個方法裏面。github
vue中的虛擬dom 補丁算法是在 snapdom 的基礎上改造而來,最主要的優化就包括針對web場景,在web中,咱們最多進行的操做是在隊尾或者隊首插入元素,在遍歷查找新的元素對應的老元素以前,先進行新老首尾2x2=4
次盲猜。web
爲了方便查看,我會在下面源碼中插入相關注釋算法
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 若是新老任何一組元素的遊標重合,就結束循環
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// 針對web中常見操做,在遍歷查找新的元素對應的老元素以前,先進行新老首尾2x2=4次盲猜
// 對比新開始和老開始
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 對比新結束和老結束
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 對比新結束和老開始
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 對比新開始和老結束
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 都沒有命中,建立一個老節點索引和key的映射表,循環查找
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// 當循環結束,若是新的列表裏面還有沒處理的元素,就所有建立新增
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 若是老的列表裏還有沒處理的元素,就所有刪除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
講到這裏你們可能仍是不明白爲何要用key呢,其實答案就在盲猜時執行的sameVnode
方法,廢話很少說,直接看代碼!app
// 判斷兩個vnode是不是相同vnode
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)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
複製代碼
當咱們在列表開頭插入一個F
元素時,若是不使用key
,則key
等於undefined
,而undefined===undefined
恆成立,而且tag
都是li
,都不是註釋節點,因此會把新插入的F
(newStartVnode)和老的A
(oldStartVnode)認爲是同一個節點執行更新,遊標向後移動一位,而且後續4個元素都按照這種邏輯更新,最後老節點先結束了(遊標先重合),新節點還剩一個元素E
,就建立一個新的節點並插入,最終一共進行了五次更新操做和一次新建插入(元素E)操做。
若是咱們使用了key,對比newStartVnode
和 oldStartVnode
時發現key不一樣,不是同一個元素,就繼續對比oldEndVnode
newEndVnode
發現他們key相同,都爲E
,而且文本內容和其餘屬性也沒變,就直接複用而後遊標向前移動一位,最後老節點先結束了(遊標先重合),新節點還剩一個元素F
,就建立一個新的節點並插入。最終只進行了一次新建插入(元素F)操做
使用key除了提升性能之外,還有一些其餘的使用場景,例如:
updateChildren
時被替換了,那麼元素的動畫可能不會完整的展現updateChildren
更新後,可能會失去焦點解決上述問題最直接最有效的辦法就是爲元素增長key
PS:下次必定要寫key 😂
最後,這篇文章屬於 從源碼解惑 系列文章中的一篇,歡迎你們關注、留言、拍磚