前言:隨之vue3.0beta版本的發佈,vue3.0正式版本相信不久就會與咱們相遇。尤玉溪在直播中也說了vue3.0的新特性typescript強烈支持,proxy響應式原理,從新虛擬dom,優化diff算法性能提高等等。小編在這裏仔細研究了vue3.0beta版本diff算法的源碼,並但願把其中的細節和奧妙和你們一塊兒分享。html
首先咱們來思考一些大中廠面試中,很容易問到的問題:vue
1 何時用到diff算法,diff算法做用域在哪裏? 2 diff算法是怎麼運做的,到底有什麼做用? 3 在v-for 循環列表 key 的做用是什麼 4 用索引index作key真的有用? 到底用什麼作key纔是最佳方案。node
若是遇到這些問題,你們是怎麼回答的呢?我相信當你讀完這篇文章,這些問題也會迎刃而解。面試
patch概念引入算法
在vue update過程當中在遍歷子代vnode的過程當中,會用不一樣的patch方法來patch新老vnode,若是找到對應的 newVnode 和 oldVnode,就能夠複用利用裏面的真實dom節點。避免了重複建立元素帶來的性能開銷。畢竟瀏覽器創造真實的dom,操縱真實的dom,性能代價是昂貴的。typescript
patch過程當中,若是面對當前vnode存在有不少chidren的狀況,那麼須要分別遍歷patch新的children Vnode和老的 children vnode。後端
存在chidren的vnode類型數組
首先思考一下什麼類型的vnode會存在children。瀏覽器
①element元素類型vnode微信
第一中狀況就是element類型vnode 會存在 children vode,此時的三個span標籤就是chidren vnode狀況
<div>
<span> 蘋果🍎 </span>
<span> 香蕉🍌 </span>
<span> 鴨梨🍐 </span>
</div>
複製代碼
在vue3.0源碼中 ,patchElement用於處理element類型的vnode
②flagment碎片類型vnode
在Vue3.0中,引入了一個fragment碎片概念。 你可能會問,什麼是碎片?若是你建立一個Vue組件,那麼它只能有一個根節點。
<template>
<span> 蘋果🍎 </span>
<span> 香蕉🍌 </span>
<span> 鴨梨🍐 </span>
</template>
複製代碼
這樣可能會報出警告,緣由是表明任何Vue組件的Vue實例須要綁定到一個單一的DOM元素中。惟一能夠建立一個具備多個DOM節點的組件的方法就是建立一個沒有底層Vue實例的功能組件。
flagment出現就是用看起來像一個普通的DOM元素,但它是虛擬的,根本不會在DOM樹中呈現。這樣咱們能夠將組件功能綁定到一個單一的元素中,而不須要建立一個多餘的DOM節點。
<Fragment>
<span> 蘋果🍎 </span>
<span> 香蕉🍌 </span>
<span> 鴨梨🍐 </span>
</Fragment>
複製代碼
在vue3.0源碼中 ,processFragment用於處理Fragment類型的vnode
從上文中咱們得知了存在children的vnode類型,那麼存在children就須要patch每個 children vnode依次向下遍歷。那麼就須要一個patchChildren方法,依次patch子類vnode。
patchChildren
vue3.0中 在patchChildren方法中有這麼一段源碼
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
/* 對於存在key的狀況用於diff算法 */
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
/* 對於不存在key的狀況,直接patch */
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
return
}
}
複製代碼
patchChildren根據是否存在key進行真正的diff或者直接patch。
既然diff算法存在patchChildren方法中,而patchChildren方法用在Fragment類型和element類型的vnode中,這樣也就解釋了diff算法的做用域是什麼。
經過前言咱們知道,存在這children的狀況的vnode,須要經過patchChildren遍歷children依次進行patch操做,若是在patch期間,再發現存在vnode狀況,那麼會遞歸的方式依次向下patch,那麼找到與新的vnode對應的vnode顯的如此重要。
咱們用兩幅圖來向你們展現vnode變化。
如上兩幅圖表示在一次更新中新老dom樹變化狀況。
假設不存在diff算法,依次按照前後順序patch會發生什麼
若是不存在diff算法,而是直接patchchildren 就會出現以下圖的邏輯。
第一次patchChidren 第二次patchChidren 第三次patchChidren‘
第四次patchChidren
若是沒有用到diff算法,而是依次patch虛擬dom樹,那麼如上稍微修改dom順序,就會在patch過程當中沒有一對正確的新老vnode,因此老vnode的節點沒有一個能夠複用,這樣就須要從新創造新的節點,浪費了性能開銷,這顯然不是咱們須要的。
那麼diff算法的做用就來了。
diff做用就是在patch子vnode過程當中,找到與新vnode對應的老vnode,複用真實的dom節點,避免沒必要要的性能開銷
在正式講diff算法以前,在patchChildren的過程當中,存在 patchKeyedChildren patchUnkeyedChildren
patchKeyedChildren 是正式的開啓diff的流程,那麼patchUnkeyedChildren的做用是什麼呢? 咱們來看看針對沒有key的狀況patchUnkeyedChildren會作什麼。
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
let i
for (i = 0; i < commonLength; i++) { /* 依次遍歷新老vnode進行patch */
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
if (oldLength > newLength) { /* 老vnode 數量大於新的vnode,刪除多餘的節點 */
unmountChildren(c1, parentComponent, parentSuspense, true, commonLength)
} else { /* /* 老vnode 數量小於於新的vnode,創造新的即誒安 */
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
commonLength
)
}
複製代碼
咱們能夠獲得結論,對於不存在key狀況 ① 比較新老children的length獲取最小值 而後對於公共部分,進行重新patch工做。 ② 若是老節點數量大於新的節點數量 ,移除多出來的節點。 ③ 若是新的節點數量大於老節點的數量,重新 mountChildren新增的節點。
那麼對於存在key狀況呢? 會用到diff算法 , diff算法作了什麼呢?
patchKeyedChildren方法究竟作了什麼? 咱們先來看看一些聲明的變量。
/* c1 老的vnode c2 新的vnode */
let i = 0 /* 記錄索引 */
const l2 = c2.length /* 新vnode的數量 */
let e1 = c1.length - 1 /* 老vnode 最後一個節點的索引 */
let e2 = l2 - 1 /* 新節點最後一個節點的索引 */
複製代碼
(a b) c (a b) d e
/* 從頭對比找到有相同的節點 patch ,發現不一樣,當即跳出*/
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
/* 判斷key ,type是否相等 */
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
parentAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
break
}
i++
}
複製代碼
第一步的事情就是從頭開始尋找相同的vnode,而後進行patch,若是發現不是相同的節點,那麼當即跳出循環。
具體流程如圖所示
isSameVNodeType
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
複製代碼
isSameVNodeType 做用就是判斷當前vnode類型 和 vnode的 key是否相等
a (b c) d e (b c)
/* 若是第一步沒有patch完,當即,從後往前開始patch ,若是發現不一樣當即跳出循環 */
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
parentAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
break
}
e1--
e2--
}
複製代碼
經歷第一步操做以後,若是發現沒有patch完,那麼當即進行第二部,從尾部開始遍歷依次向前diff。
若是發現不是相同的節點,那麼當即跳出循環。
具體流程如圖所示
③④主要針對新增和刪除元素的狀況,前提是元素沒有發生移動, 若是有元素髮生移動就要走⑤邏輯。
(a b) (a b) c i = 2, e1 = 1, e2 = 2 (a b) c (a b) i = 0, e1 = -1, e2 = 0
/* 若是新的節點大於老的節點數 ,對於剩下的節點所有以新的vnode處理( 這種狀況說明已經patch完相同的vnode ) */
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch( /* 建立新的節點*/
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG
)
i++
}
}
}
複製代碼
i > e1
若是新的節點大於老的節點數 ,對於剩下的節點所有以新的vnode處理( 這種狀況說明已經patch完相同的vnode ),也就是要所有create新的vnode.
具體邏輯如圖所示
i > e2
(a b) c
(a b)
i = 2, e1 = 2, e2 = 1
a (b c)
(b c)
i = 0, e1 = 0, e2 = -1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
複製代碼
對於老的節點大於新的節點的狀況 ,對於超出的節點所有卸載 ( 這種狀況說明已經patch完相同的vnode )
具體邏輯如圖所示
diff核心
在①②狀況下沒有遍歷完的節點以下圖所示。
剩下的節點。
const s1 = i //第一步遍歷到的index
const s2 = i
const keyToNewIndexMap: Map<string | number, number> = new Map()
/* 把沒有比較過的新的vnode節點,經過map保存 */
for (i = s2; i <= e2; i++) {
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
let j
let patched = 0
const toBePatched = e2 - s2 + 1 /* 沒有通過 path 新的節點的數量 */
let moved = false /* 證實是否 */
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
/* 創建一個數組,每一個子元素都是0 [ 0, 0, 0, 0, 0, 0, ] */
複製代碼
遍歷全部新節點把索引和對應的key,存入map keyToNewIndexMap中
keyToNewIndexMap 存放 key -> index 的map
D : 2 E : 3 C : 4 I : 5
接下來聲明一個新的指針 j,記錄剩下新的節點的索引。 patched ,記錄在第⑤步patched新節點過的數量 toBePatched 記錄⑤步以前,沒有通過patched 新的節點的數量。 moved表明是否發生過移動,我們的demo是已經發生過移動的。
newIndexToOldIndexMap 用來存放新節點索引和老節點索引的數組。 newIndexToOldIndexMap 數組的index是新vnode的索引 , value是老vnode的索引。
接下來
for (i = s1; i <= e1; i++) { /* 開始遍歷老節點 */
const prevChild = c1[i]
if (patched >= toBePatched) { /* 已經patch數量大於等於, */
/* ① 若是 toBePatched新的節點數量爲0 ,那麼統一卸載老的節點 */
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
/* ② 若是,老節點的key存在 ,經過key找到對應的index */
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else { /* ③ 若是,老節點的key不存在 */
for (j = s2; j <= e2; j++) { /* 遍歷剩下的全部新節點 */
if (
newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新節點沒有被patch */
isSameVNodeType(prevChild, c2[j] as VNode)
) { /* 若是找到與當前老節點對應的新節點那麼 ,將新節點的索引,賦值給newIndex */
newIndex = j
break
}
}
}
if (newIndex === undefined) { /* ①沒有找到與老節點對應的新節點,刪除當前節點,卸載全部的節點 */
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
/* ②把老節點的索引,記錄在存放新節點的數組中, */
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
/* 證實有節點已經移動了 */
moved = true
}
/* 找到新的節點進行patch節點 */
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
patched++
}
}
複製代碼
這段代碼算是diff算法的核心。
第一步: 經過老節點的key找到對應新節點的index:開始遍歷老的節點,判斷有沒有key, 若是存在key經過新節點的keyToNewIndexMap找到與新節點index,若是不存在key那麼會遍歷剩下來的新節點試圖找到對應index。
第二步:若是存在index證實有對應的老節點,那麼直接複用老節點進行patch,沒有找到與老節點對應的新節點,刪除當前老節點。
第三步:newIndexToOldIndexMap找到對應新老節點關係。
到這裏,咱們patch了一遍,把全部的老vnode都patch了一遍。
如圖所示 可是接下來的問題。
1 雖然已經patch過全部的老節點。能夠對於已經發生移動的節點,要怎麼真正移動dom元素。 2 對於新增的節點,(圖中節點I)並無處理,應該怎麼處理。
/*移動老節點建立新節點*/
/* 根據最長穩定序列移動相對應的節點 */
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) { /* 沒有老的節點與新的節點對應,則建立一個新的vnode */
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG
)
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) { /*若是沒有在長*/
/* 須要移動的vnode */
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
複製代碼
首選經過getSequence獲得一個最長穩定序列,對於index === 0 的狀況也就是新增節點(圖中I) 須要重新mount一個新的vnode,而後對於發生移動的節點進行統一的移動操做
什麼叫作最長穩定序列
對於如下的原始序列 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 最長遞增子序列爲 0, 2, 6, 9, 11, 15.
爲何要獲得最長穩定序列
由於咱們須要一個序列做爲基礎的參照序列,其餘未在穩定序列的節點,進行移動。
通過上述咱們大體知道了diff算法的流程
1 從頭對比找到有相同的節點 patch ,發現不一樣,當即跳出。
2若是第一步沒有patch完,當即,從後往前開始patch ,若是發現不一樣當即跳出循環。
3若是新的節點大於老的節點數 ,對於剩下的節點所有以新的vnode處理( 這種狀況說明已經patch完相同的vnode )。
4 對於老的節點大於新的節點的狀況 , 對於超出的節點所有卸載 ( 這種狀況說明已經patch完相同的vnode )。
5不肯定的元素( 這種狀況說明沒有patch完相同的vnode ) 與 3 ,4對立關係。
1 把沒有比較過的新的vnode節點,經過map保存
記錄已經patch的新節點的數量 patched 沒有通過 path 新的節點的數量 toBePatched 創建一個數組newIndexToOldIndexMap,每一個子元素都是[ 0, 0, 0, 0, 0, 0, ] 裏面的數字記錄老節點的索引 ,數組索引就是新節點的索引
開始遍歷老節點
① 若是 toBePatched新的節點數量爲0 ,那麼統一卸載老的節點.
② 若是,老節點的key存在 ,經過key找到對應的index
③ 若是,老節點的key不存在 1 遍歷剩下的全部新節點 2 若是找到與當前老節點對應的新節點那麼 ,將新節點的索引,賦值給newIndex
④ 沒有找到與老節點對應的新節點,卸載當前老節點。
⑤ 若是找到與老節點對應的新節點,把老節點的索引,記錄在存放新節點的數組中,
1 若是節點發生移動 記錄已經移動了 2 patch新老節點 找到新的節點進行patch節點
遍歷結束
若是發生移動
① 根據 newIndexToOldIndexMap 新老節點索引列表找到最長穩定序列
② 對於 newIndexToOldIndexMap -item =0 證實不存在老節點 ,重新造成新的vnode
③ 對於發生移動的節點進行移動處理。
複製代碼
在咱們上述diff算法中,經過isSameVNodeType方法判斷,來判斷key是否相等判斷新老節點。 那麼由此咱們能夠總結出?
在v-for循環中,key的做用是:經過判斷newVnode和OldVnode的key是否相等,從而複用與新節點對應的老節點,節約性能的開銷。
用index作key的效果實際和沒有用diff算法是同樣的,爲何這麼說呢,下面我就用一幅圖來講明:
若是所示當咱們用index做爲key的時候,不管咱們怎麼樣移動刪除節點,到了diff算法中都會從頭至尾依次patch(圖中:全部節點均未有效的複用)
當已用index拼接其餘值做爲索引的時候,由於每個節點都找不到對應的key,致使全部的節點都不能複用,全部的新vnode都須要從新建立。都須要從新create
如圖所示。
如圖所示。每個節點都作到了複用。起到了diff算法的真正做用。
咱們在上面,已經把剛開始的問題通通解決了,最後用一張思惟腦圖來重新整理一下整個流程。diff算法,你學會了嗎? 微信掃碼關注公衆號,按期分享技術文章