[vue3] vue3更新組件流程與diff算法
在Vue3中,組件的更新通過patch函數(shù)進行處理。
patch函數(shù)
源碼位置:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {
// 二者相同,不需要更新
if (n1 === n2) {
return
}
// vnode類型不同,直接卸載舊節(jié)點
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// ......
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
// 處理文字節(jié)點
break
case Comment:
// 處理注釋節(jié)點
break
case Static:
// 靜態(tài)節(jié)點
break
case Fragment:
// Fragment節(jié)點
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 處理普通DOM元素
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 處理組件
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 處理teleport
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 處理suspense
} else if (__DEV__) {
// 報錯:vnode類型不在可識別范圍內(nèi)
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
}
patch函數(shù)用來掛載或者更新vnode。
patch的大致流程:
n1和n2如果相等,則表示無變化,直接退出;n1和n2如果引用不同,則先檢查其vnode類型,如果類型不同,則直接卸載n1,掛載n2;- 主流程:根據(jù)
n1和n2的vnode類型,調(diào)用不同的process函數(shù)。
process
process函數(shù)的參數(shù)列表大致相同,都是要傳入n1、n2和container等參數(shù)。patch函數(shù)主要起到一個分類討論的功能。
這里只討論普通元素類型和組件類型的vnode處理過程,因為這是Vue應用中最常見、覆蓋范圍最廣的兩種類型。
普通元素類型,即
ShapeFlags.ELEMENT,在瀏覽器環(huán)境下就是指DOM類型。
普通元素vs組件
從組件樹的角度來理解普通元素和組件元素的區(qū)別。
一個組件的children可以是普通元素或組件元素。
-
葉子節(jié)點必須是普通元素,因為只有普通元素能夠通過相關(guān)平臺掛載到界面上。
Vue會在編譯時確定mount方法,以適應不同的平臺。對于瀏覽器環(huán)境來說,普通元素是通過vnode來表示DOM節(jié)點,將vnode轉(zhuǎn)換成實際DOM元素并插入到頁面上的操作由vue3源碼中的runtime-dom這個package實現(xiàn)。葉子節(jié)點必須是普通元素,但是普通元素不一定是葉子節(jié)點,比如一個
div標簽內(nèi)部可以包含其它組件。 -
葉子節(jié)點不可能是組件,因為組件必須被實現(xiàn)且被注冊,其實現(xiàn)必須使用已注冊的組件或者普通元素。并且組件是虛擬元素,并不能被實際掛載到指定平臺上,只能遞歸地
patch它的children,直到把普通元素都掛載到界面上。
在 Vue3 - patch 函數(shù)的源碼中可以看到除了這兩種類型,還有很多針對其它類型 vnode 的 process 函數(shù),這些 process 函數(shù)主要做的只有兩件事:掛載和更新。
對于舊vnode n1 和 新vnode n2:
- 當
n1是null時,則表示掛載n2; - 當
n1不為null時,則表示n1更新為n2;
patchElement
patchElement 會對它的 children 也進行 patch,也就是調(diào)用 patchChildren 函數(shù)。
children 有三種情況:文本、數(shù)組、NULL。

diff算法
diff 算法用于將舊children的vnode數(shù)組更新為新children的vnode數(shù)組,它通過比較兩個序列,盡可能地復用相同的vnode,以此來減少頻繁創(chuàng)建vnode帶來的開銷。
事實上,diff 是在 patchKeyedChildren 中實現(xiàn)的,對于沒有設(shè)置 key 的數(shù)組,patchChildren函數(shù)內(nèi)部調(diào)用的是 patchUnkeyedChildren,函數(shù)實現(xiàn)大致如下:
- 計算兩個數(shù)組的長度的最小值
commonLength;- 前
commonLength個 vnode 直接 patch 更新,不會考慮移動到不同位置來復用;- 舊序列如果有剩余則unmount,新序列如果有多余則mount。
這種做法在大多數(shù)情況下都會需要創(chuàng)建 vnode,開銷還是比較大的。因此為了提高渲染性能,使用渲染列表的時候要寫上 key。
![]()
vue3 的 diff算法實現(xiàn)在patchKeyedChildren函數(shù)中,主要包含五個流程,其中第五個是最復雜的步驟:
-
兩個序列從頭部向尾部依次同步,直到不能匹配進入下個流程;
起始索引都是從0開始,用一個變量
i就可以了。
-
兩個序列從尾部向頭部依次同步,直到不能匹配進入下個流程;
兩個序列長度可能不一樣,最后一個元素的索引不一樣,因此需要兩個變量
e1和e2來指向ending index。
在上述兩個流程之后:
-
如果舊序列遍歷完了,而新序列還有剩余,則新序列剩余的vnode依次mount;
i>e1則表示舊序列遍歷完了;i<=e2則表示新序列還有剩余;while(i<=e2){...; i++}把剩余的vnode都掛載。
-
如果新序列遍歷完了,而舊序列還有剩余,則舊序列剩余的vnode依次unmount;
i>e2表示新序列都遍歷完了;i<=e1表示舊序列還有剩余;while(i<=e1){ unmount(...); i++ }將剩余的舊 vnode 都卸載。
-
未知序列,盡可能地通過移動復用vnode,剩下的mount或者unmount。
頭部和尾部都同步了若干vnode,但是兩個序列都還沒有遍歷完成,說明中間有一段序列是混亂的、難以匹配的。
在步驟5中,有細分為多個子步驟:
首先用
s1和s2表示舊新序列的起始索引:const s1 = s2 = i;5.1. 遍歷新序列,使用
Map建立key到newIndex的映射:keyToNewIndexMap;使用
Map的原因是PropertyKey這個類型是聯(lián)合類型string | number | symbol,不能簡單的用對象或數(shù)組表示。建立 key 到 index 的映射,是為了后續(xù)我們可以通過舊序列中的 key 來建立可復用情況下新舊節(jié)點之間的映射關(guān)系。
// 這段代碼不是源碼,只保留主干。 const keyToNewIndexMap: Map<PropertyKey, number> = new Map() for (i = s2; i <= e2; i++) { const nextChild = c2[i]; // c2 即 children2 if (nextChild.key != null) keyToNewIndexMap.set(nextChild.key, i) } }5.2. 遍歷舊序列,使用一個
newIndexToOldIndexMap數(shù)組建立新舊序列中可復用節(jié)點的位置對應關(guān)系。newIndexToOldIndexMap的作用:
通過這個數(shù)組,我們可以知道一個新vnode可以由哪個舊vnode更新得到。在這個數(shù)組中,newIndex是以 0 開始的,而 oldIndex 是以 1 開始的,這是為了把
oldIndex==0作為一個特殊標識,表示新節(jié)點在舊序列中不存在。當newIndexToOldIndexMap[k] = 0,則表示新序列中第 k 個vnode在舊序列中不存在,無法復用。
newIndexToOldIndexMap的構(gòu)建過程:
遍歷舊序列
c1:for(let i=s1; i<=e1; i++){...}-
使用
keyToNewIndexMap查詢c1[i]的 key:-
如果 key 為 undefined,則說明這個舊的 vnode 在新序列中不存在了,卸載這個舊 vnode;
-
如果 key 為某個數(shù)字,則表明這個舊 vnode 在新序列中有 vnode 的 key 跟它一樣,可以復用。使用
patch函數(shù)將舊節(jié)點更新為新節(jié)點。這一步驟中記錄新舊序列索引映射的代碼是
newIndexToOldIndexMap[newIndex - s2] = i + 1。- 減去
s2是因為序列包含 diff 算法步驟1同步的頭節(jié)點; i+1是因為這個數(shù)組記錄的 oldIndex 是從 1 開始的。
- 減去
-
-
如果發(fā)現(xiàn)新序列中的節(jié)點都找到與之對應的舊節(jié)點了,那么
for循環(huán)后續(xù)的舊節(jié)點都直接卸載。
在這個步驟中,還通過一個
moved變量來記錄節(jié)點的相對位置是否被移動了:if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { moved = true }如果當前新節(jié)點的索引
newIndex大于或等于此前遍歷到的最大新節(jié)點索引maxNewIndexSoFar,那么當前節(jié)點在新列表中的順序相對于舊列表來說是保持遞增的。
-
? 5.3 移動與掛載
? 經(jīng)過上面若干步驟,能復用的舊節(jié)點都通過 patch 將數(shù)據(jù)更新到新節(jié)點上了,不能復用的舊節(jié)點都被卸載了。
? 而新節(jié)點如果沒有在舊序列中出現(xiàn),則掛載;如果在舊序列中出現(xiàn)了,
-
如果
moved為 false,則表示(已經(jīng)執(zhí)行復用操作的)新節(jié)點的順序和在舊序列中的相對順序是一致的,這種情況無需處理; -
如果
moved為 true,則表示相對順序不一致,需要移動 vnode。為了減少移動次數(shù),這里應用了最長遞增子序列算法,計算了數(shù)組
newIndexToOldIndexMap的最長遞增子序列。從上圖右邊的子圖中可以看出,遞增子序列越長意味著相對順序一致的子序列越長,那么需要移動的 vnode 就越少。
思考版圖大致如下:


浙公網(wǎng)安備 33010602011771號