vue源碼解讀子節(jié)點(diǎn)優(yōu)化更新
前言
Vue中更新節(jié)點(diǎn),當(dāng)新 VNode 和舊 VNode 都是元素節(jié)點(diǎn)且都有子節(jié)點(diǎn)時,Vue會循環(huán)對比新舊 VNode 的子節(jié)點(diǎn)數(shù)組,然后根據(jù)不同情況做不同處理。
雖然這種方法能解決問題,但是當(dāng)更新子節(jié)點(diǎn)特別多時,循環(huán)算法的時間復(fù)雜度就會很高,所以Vue對此進(jìn)行了優(yōu)化。
優(yōu)化前存在的問題
現(xiàn)在有新的 newChildren 數(shù)組和舊的 oldChildren 數(shù)組:
newChildren = ['a','b','c','d']; oldChildren = ['a','b','c','e'];
按照之前的解決方案:先循環(huán) newChildren 數(shù)組,把第一個節(jié)點(diǎn)與 oldChildren 里的子節(jié)點(diǎn)逐一對比,再根據(jù)情況去處理。如果像上面的代碼一樣,前三個子節(jié)點(diǎn)都沒有變化,只修改了最后一個子節(jié)點(diǎn),但因?yàn)檠h(huán)查找,還是要循環(huán)16次才能發(fā)現(xiàn),所以前面做的15次循環(huán)全是無用功。
優(yōu)化策略分析
Vue的策略是不按照循序去循環(huán) newChildren 和 oldChildren 這兩個數(shù)組,而是先去比較特殊位置的子節(jié)點(diǎn),比如:
- 把
newChildren數(shù)組里的第一個未處理子節(jié)點(diǎn)和oldChildren數(shù)組的第一個未處理子節(jié)點(diǎn)做對比,如果相同,就更新節(jié)點(diǎn)。 - 如果不同,把
newChildren數(shù)組里最后一個未處理子節(jié)點(diǎn)和oldChildren數(shù)組里最后一個未處理子節(jié)點(diǎn)做比對,如果相同,就更新節(jié)點(diǎn)。 - 如果不同,把
newChildren數(shù)組里最后一個未處理子節(jié)點(diǎn)和oldChildren數(shù)組里第一個未處理子節(jié)點(diǎn)做比對,如果相同,就更新節(jié)點(diǎn)。 - 如果不同,把
newChildren數(shù)組里第一個未處理子節(jié)點(diǎn)和oldChildren數(shù)組里最后一個未處理子節(jié)點(diǎn)做比對,如果相同,就更新節(jié)點(diǎn)。 - 如果四種情況試完如果還不同,就按照之前循環(huán)的方式來查找節(jié)點(diǎn)。

四種情況分別分別被稱作:
不相同才往后繼續(xù)。
- 新前與舊前
- 如果相同,直接更新,因?yàn)槲恢靡蚕嗤瑹o需移動。
- 新后與舊后
- 如果相同,直接更新,因?yàn)槲恢靡蚕嗤?,無需移動。
- 新后與舊前
- 如果相同,更新,但因?yàn)槲恢貌煌?,所以需要移動位?/li>
- 新前與舊后
- 如果相同,更新,但因?yàn)槲恢貌煌孕枰苿游恢?/li>
如果上面的情況都不滿足,再通過之前的循環(huán)方式查找
源碼解析
從上面的優(yōu)化策略中,知道對比子節(jié)點(diǎn)是先對比特殊位置的子節(jié)點(diǎn),對比成功就進(jìn)行更新處理,也就是說有可能處理第一個,也有可能是處理最后一個,所以在循環(huán)的時候就不可能只是從前往后循環(huán),而是從兩邊向中間循環(huán)。

首先定義四個變量
- newStartIdx:新子節(jié)點(diǎn)數(shù)組里開始位置的下標(biāo);
- newEndIdx:新子節(jié)點(diǎn)數(shù)組里結(jié)束位置的下標(biāo);
- oldStartIdx:舊子節(jié)點(diǎn)數(shù)組里開始位置的下標(biāo);
- oldEndIdx:舊子節(jié)點(diǎn)數(shù)組里結(jié)束位置的下標(biāo);
在循環(huán)的時候,每處理一個節(jié)點(diǎn),就將下標(biāo)向圖中箭頭的方向移動一個位置,newStartIdx和 oldStartIdx 往后加1,newEndIdx和 oldEndIdx往前減1。
理解了這個概念后,就可以解析源碼了:
定義需要的變量
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // oldChildren開始索引
let oldEndIdx = oldCh.length - 1 // oldChildren結(jié)束索引
let oldStartVnode = oldCh[0] // oldChildren中所有未處理節(jié)點(diǎn)中的第一個
let oldEndVnode = oldCh[oldEndIdx] // oldChildren中所有未處理節(jié)點(diǎn)中的最后一個
?
let newStartIdx = 0 // newChildren開始索引
let newEndIdx = newCh.length - 1 // newChildren結(jié)束索引
let newStartVnode = newCh[0] // newChildren中所有未處理節(jié)點(diǎn)中的第一個
let newEndVnode = newCh[newEndIdx] // newChildren中所有未處理節(jié)點(diǎn)中的最后一個A
}
如果 oldStartVNode 不存在,則跳過,將 oldStartIdx 加1,對比下一個
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx];
}
}
如果oldEndVnode不存在,則跳過,將oldEndIdx減1,比對前一個
else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
}
如果新前與舊前節(jié)點(diǎn)相同,就把兩個節(jié)點(diǎn)進(jìn)行patch更新,同時oldStartIdx和newStartIdx都加1,后移一個位置
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
如果新后與舊后節(jié)點(diǎn)相同,就把兩個節(jié)點(diǎn)進(jìn)行patch更新,同時oldEndIdx和newEndIdx都減1,前移一個位置
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
如果新后與舊前節(jié)點(diǎn)相同,先把兩個節(jié)點(diǎn)進(jìn)行patch更新,然后把舊前節(jié)點(diǎn)移動到oldChilren中所有未處理節(jié)點(diǎn)之后,最后把oldStartIdx加1,后移一個位置,newEndIdx減1,前移一個位置
else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
如果新前與舊后節(jié)點(diǎn)相同,先把兩個節(jié)點(diǎn)進(jìn)行patch更新,然后把舊后節(jié)點(diǎn)移動到oldChilren中所有未處理節(jié)點(diǎn)之前,最后把newStartIdx加1,后移一個位置,oldEndIdx減1,前移一個位置
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
- 不屬于以上四種情況,就進(jìn)行常規(guī)的循環(huán)比對
patch。
如果oldStartIdx大于oldEndIdx了,那就表示oldChildren比newChildren先循環(huán)完畢,那么newChildren里面剩余的節(jié)點(diǎn)都是需要新增的節(jié)點(diǎn),把[newStartIdx, newEndIdx]之間的所有節(jié)點(diǎn)都插入到OldChildren中。
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
如果newStartIdx大于newEndIdx了,那就表示newChildren比oldChildren先循環(huán)完畢,那么oldChildren里面剩余的節(jié)點(diǎn)都是需要刪除的節(jié)點(diǎn),把[oldStartIdx, oldEndIdx]之間的所有節(jié)點(diǎn)都刪除
else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
小結(jié)
- 分析了循環(huán)更新子節(jié)點(diǎn)存在的性能問題,數(shù)據(jù)量大時,時間復(fù)雜度高。
- 分析Vue中的優(yōu)化策略,先對比特殊位置的子節(jié)點(diǎn),分別是:新前與舊前、新后與舊后、新后與舊前、新前與舊后。如果都不相同,在通過循環(huán)遍歷對比。
- 理解源碼,通過源碼解析,在腦海中繪制一條清晰的思路線。
以上就是vue源碼解讀子節(jié)點(diǎn)優(yōu)化更新的詳細(xì)內(nèi)容,更多關(guān)于Vue子節(jié)點(diǎn)更新的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue-router?導(dǎo)航完成后獲取數(shù)據(jù)的實(shí)現(xiàn)方法
這篇文章主要介紹了vue-router?導(dǎo)航完成后獲取數(shù)據(jù),通過使用生命周期的 created() 函數(shù),在組件創(chuàng)建完成后調(diào)用該方法,本文結(jié)合實(shí)例代碼給大家講解的非常詳細(xì)需要的朋友可以參考下2022-11-11
Vue2.0基于vue-cli+webpack同級組件之間的通信教程(推薦)
下面小編就為大家?guī)硪黄猇ue2.0基于vue-cli+webpack同級組件之間的通信教程(推薦)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09
Vite創(chuàng)建項目的實(shí)現(xiàn)步驟
隨著 Vite2 的發(fā)布并日趨穩(wěn)定,現(xiàn)在越來越多的項目開始嘗試使用它。本文我們就介紹了Vite創(chuàng)建項目的實(shí)現(xiàn)步驟,感興趣的可以了解一下2021-07-07
Vue.js實(shí)戰(zhàn)之使用Vuex + axios發(fā)送請求詳解
這篇文章主要給大家介紹了關(guān)于Vue.js使用Vuex與axios發(fā)送請求的相關(guān)資料,文中介紹的非常詳細(xì),相信對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-04-04
vue實(shí)現(xiàn)el-select默認(rèn)選擇第一個或者第二個
這篇文章主要介紹了vue實(shí)現(xiàn)el-select默認(rèn)選擇第一個或者第二個,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09

