詳解key在Vue列表渲染時究竟起到了什么作用
Vue2+采用diff算法來進行新舊vnode的對比從而更新DOM節(jié)點。而通常在我們使用v-for這個指令的時候,Vue會要求你給循環(huán)列表的每一項添加唯一的key,那么這個key在渲染列表時究竟起到了什么作用呢?
在解釋這一點之前,你最好已經了解Vue的diff算法的具體原理是什么。
Vue2更新真實DOM的操作主要是兩種:創(chuàng)建新DOM節(jié)點并移除舊DOM節(jié)點和更新已存在的DOM節(jié)點,這兩種方式里創(chuàng)建新DOM節(jié)點的開銷肯定是遠大于更新或移動已有的DOM節(jié)點,所以在diff中邏輯都是為了減少新的創(chuàng)建而更多的去復用已有DOM節(jié)點來完成DOM的更新。
在新舊vnode的diff過程中,key是判斷兩個節(jié)點是否為同一節(jié)點的首要條件:
// 參見Vue2源碼 core/vdom/patch.js
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)
)
)
)
}
值得注意的是,如果新舊vnode的key值都未定義的話那么兩個key都為undefined,a.key === b.key 是成立的
接下來是在updateChildren方法中,這個方法會對新舊vnode進行diff,然后將比對出的結果用來更新真實的DOM
// 參見Vue2源碼 core/vdom/patch.js
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
...
} else if (isUndef(oldEndVnode)) {
...
} else if (sameVnode(oldStartVnode, newStartVnode)) {
...
} else if (sameVnode(oldEndVnode, newEndVnode)) {
...
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
...
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
...
} else {
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 {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
...
}
設置key的可以在diff中更快速的找到對應節(jié)點,提高diff速度
在updateChildren方法的while循環(huán)中,如果頭尾交叉對比沒有結果,即oldStartVnode存在且oldEndVnode存在且新舊children首尾四個vnode互不相同的條件下,會根據(jù)newStartVnode的key去對比oldCh數(shù)組中的key,從而找到相應oldVnode
首先通過createKeyToOldIdx方法創(chuàng)建一個關于oldCh的map
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
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
}
這個map中將所有定義了key的oldVnode在數(shù)組中的index值作為鍵值,它的key作為鍵名存儲起來,然后賦給oldKeyToIdx
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
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
}
}
如果newStartVnode的key存在的話,就去oldKeyToIdx中尋找相同key所對應的index值,這樣就能拿到跟newStartVnode的key相同的oldVnode在oldCh數(shù)組中的index,即得到了與newStartVnode對應的oldVnode。如果找不到的話,那么idxInOld就為undefined。
而如果newStartVnode并沒有設置key,則通過findIdxInOld方法遍歷oldCh來獲取與newStartVnode互為sameVnode的oldVnode,返回這個oldVnode在oldCh數(shù)組的index。(前面介紹過,Vue在更新真實DOM時傾向于真實DOM節(jié)點的復用,所以在這里還是會選擇去找對應的oldVnode,來更新已有的DOM節(jié)點)
這時候設置key的好處就顯而易見了,有key存在時我們可以通過map映射快速定位到對應的oldVnode然后進行patch,沒有key值時我們需要遍歷這個oldCh數(shù)組然后去一一進行比較,相比之下肯定是key存在時diff更高效。
接下來就是更新DOM的過程,如果oldCh[idxInOld]存在且與newStartVnode互為sameVnode存在則先更新再移動,否則創(chuàng)建新的element
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 {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
那么設置key值就一定能提高diff效率嗎?
答案是否定的
`<div v-for="i in arr">{{ i }}</div>`
// 如果我們的數(shù)組是這樣的
[1, 2, 3, 4, 5]
// 它的渲染結果是這樣的
`<div>1</div>` // key: undefined
`<div>2</div>` // key: undefined
`<div>3</div>` // key: undefined
`<div>4</div>` // key: undefined
`<div>5</div>` // key: undefined
// 將它打亂
[4, 1, 3, 5, 2]
// 渲染結果是這樣的 期間只發(fā)生了DOM節(jié)點的文本內容的更新
`<div>4</div>` // key: undefined
`<div>1</div>` // key: undefined
`<div>3</div>` // key: undefined
`<div>5</div>` // key: undefined
`<div>2</div>` // key: undefined
// 如果我們給這個數(shù)組每一項都設置了唯一的key
[{id: 'A', value: 1}, {id: 'B', value: 2}, {id: 'C', value: 3}, {id: 'D', value: 4}, {id: 'E', value: 5}]
// 它的渲染結果應該是這樣的
`<div>1</div>` // key: A
`<div>2</div>` // key: B
`<div>3</div>` // key: C
`<div>4</div>` // key: D
`<div>5</div>` // key: E
// 將它打亂
[{id: 'D', value: 4}, {id: 'A', value: 1}, {id: 'C', value: 3}, {id: 'E', value: 5}, {id: 'B', value: 2}]
// 渲染結果是這樣的 期間只發(fā)生了DOM節(jié)點的移動
`<div>4</div>` // key: D
`<div>1</div>` // key: A
`<div>3</div>` // key: C
`<div>5</div>` // key: E
`<div>2</div>` // key: B
我們給數(shù)組設置了key之后數(shù)組的diff效率真的變高了嗎?
并沒有,因為在簡單模板的數(shù)組渲染中,新舊節(jié)點的key都為undefined,根據(jù)sameVnode的判斷條件,這些新舊節(jié)點的key、tag等屬性全部相同,所以在sameVnode(oldStartVnode, newStartVnode)這一步的時候就已經判定為對應的節(jié)點(不再執(zhí)行頭尾交叉對比),然后直接進行patchVnode,根本沒有走后面的那些else。每一次循環(huán)新舊節(jié)點都是相對應的,只需要更新其內的文本內容就可以完成DOM更新,這種原地復用的效率無疑是最高的。
而當我們設置了key之后,則會根據(jù)頭尾交叉對比結果去執(zhí)行下面的if else,進行判斷之后還需要執(zhí)行insertBefore等方法移動真實DOM的節(jié)點的位置或者進行DOM節(jié)點的添加和刪除,這樣的查找復用開銷肯定要比不帶key直接原地復用的開銷要高。
Vue文檔中對此也進行了說明:
當 Vue.js 用 v-for 正在更新已渲染過的元素列表時,它默認用“就地復用”策略。如果數(shù)據(jù)項的順序被改變,Vue 將不會移動 DOM 元素來匹配數(shù)據(jù)項的順序, 而是簡單復用此處每個元素,并且確保它在特定索引下顯示已被渲染過的每個元素。
這個默認的模式是高效的,但是只適用于不依賴子組件狀態(tài)或臨時 DOM 狀態(tài) (例如:表單輸入值) 的列表渲染輸出。
建議盡可能在使用 v-for 時提供 key,除非遍歷輸出的 DOM 內容非常簡單,或者是刻意依賴默認行為以獲取性能上的提升。
所以,簡單列表的渲染可以不使用key或者用數(shù)組的index作為key(效果等同于不帶key),這種模式下性能最高,但是并不能準確的更新列表項的狀態(tài)。一旦你需要保存列表項的狀態(tài),那么就需要用使用唯一的key用來準確的定位每一個列表項以及復用其自身的狀態(tài),而大部分情況下列表組件都有自己的狀態(tài)。
總結
key在列表渲染中的作用是:在復雜的列表渲染中快速準確的找到與newVnode相對應的oldVnode,提升diff效率
以上所述是小編給大家介紹的key在Vue列表渲染時究竟起到了什么作用詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網站的支持!
相關文章
vue2 拖動排序 vuedraggable組件的實現(xiàn)
這篇文章主要介紹了vue2 拖動排序 vuedraggable組件的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-08-08
elementUI表格多選框this.$refs.xxx.toggleRowSelection無效問題
這篇文章主要介紹了elementUI表格多選框this.$refs.xxx.toggleRowSelection無效問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11
vue.js中v-on:textInput無法執(zhí)行事件問題的解決過程
大家都知道vue.js通過v-on完成事件處理與綁定,但最近使用v-on的時候遇到了一個問題,所以下面這篇文章主要給大家介紹了關于vue.js中v-on:textInput無法執(zhí)行事件問題的解決過程,需要的朋友可以參考下。2017-07-07
vue動態(tài)的 BreadCrumb 組件el-breadcrumb ElementUI詳解
這篇文章主要介紹了vue如何做一個動態(tài)的 BreadCrumb 組件,el-breadcrumb ElementUI2024-07-07
,本文通過圖文示例代碼相結合給大家介紹的非常詳細,需要的朋友可以參考下
解決Vue-cli npm run build生產環(huán)境打包,本地不能打開的問題
今天小編就為大家分享一篇解決Vue-cli npm run build生產環(huán)境打包,本地不能打開的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09

