Vue3diff算法原理和優(yōu)化
關(guān)注前端公眾號 【前端每日一博】
前言
面試官:
你知道 Vue3.0 Object.difineProperty和vue3.0的proxy的區(qū)別嗎?
你知道 Vue3.0 diff算法原理和它有什么優(yōu)化嗎?
你知道 Vue3.0 proxy是如何通知相關(guān)組件進(jìn)行重新渲染嗎?
...
你:。。。
隨著vue3.0beta版本的發(fā)布,vue3.0正式版本相信不久就會(huì)與我們相遇。尤雨溪在直播中也說了vue3.0的新特性typescript強(qiáng)烈支持,proxy響應(yīng)式原理,重新虛擬dom,優(yōu)化diff算法性能提升等等??上攵?,Vue3.0時(shí)代有一大波我們需要了解的東西。
「廢話不多說,今天的主題是和大家分享一下Vue 虛擬Dom、Diff算法原理以及源碼解析、以及Vue3.0的diff算法是如何進(jìn)行優(yōu)化的?!?/strong>
其他系列在此:
「Vue3.0時(shí)代你必須了解的:響應(yīng)式原理」
「Vue3.0時(shí)代你必須了解的:compositon api 用法和注意事項(xiàng)」
「基于項(xiàng)目實(shí)戰(zhàn)闡述vue3.0新型狀態(tài)管理和邏輯復(fù)用方式」
目錄
virtual dom diff算法原理 Vue3.0 diff算法優(yōu)化
virtual dom
如果咱們不了解virtual dom 的話,要理解DIff算法是比較困難的。
概念:
template 渲染函數(shù) vnode(virtual dom) patch(diff算法) view

Vue.js通過編譯將template 模板轉(zhuǎn)換成渲染函數(shù)(render ) ,執(zhí)行渲染函數(shù)就可以得到一個(gè)虛擬節(jié)點(diǎn)樹
VNode 虛擬節(jié)點(diǎn):它可以代表一個(gè)真實(shí)的 dom 節(jié)點(diǎn)。通過 createElement 方法能將 VNode 渲染成 dom 節(jié)點(diǎn)。簡單地說,vnode可以理解成節(jié)點(diǎn)描述對象,它描述了應(yīng)該怎樣去創(chuàng)建真實(shí)的DOM節(jié)點(diǎn)。
patch(也叫做patching算法):虛擬DOM最核心的部分,它可以將vnode渲染成真實(shí)的DOM,這個(gè)過程是對比新舊虛擬節(jié)點(diǎn)之間有哪些不同,然后根據(jù)對比結(jié)果找出需要更新的的節(jié)點(diǎn)進(jìn)行更新。這點(diǎn)我們從單詞含義就可以看出, patch本身就有補(bǔ)丁、修補(bǔ)的意思,其實(shí)際作用是在現(xiàn)有DOM上進(jìn)行修改來實(shí)現(xiàn)更新視圖的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的實(shí)現(xiàn),并在些基礎(chǔ)上作了很多的調(diào)整和改進(jìn)。
什么是virtual dom
通俗易懂的來說就是用一個(gè)簡單的對象去代替復(fù)雜的dom對象。
如果你去打印一下一個(gè)真實(shí)的DOM節(jié)點(diǎn),就會(huì)發(fā)現(xiàn)DOM節(jié)點(diǎn)上有很多屬性,如果Vue每次都生成一個(gè)新的真實(shí)DOM節(jié)點(diǎn),對性能是巨大的浪費(fèi)。
Virtual DOM 實(shí)際上就是以JavaScript對象(VNode )為基礎(chǔ)形成一棵樹,對真實(shí)DOM的一層抽象。Vue最終的工作就是通過這棵樹批量生成真實(shí)的DOM節(jié),可以說兩者存在一層映射關(guān)系。
簡單來說,可以把Virtual DOM 理解為一個(gè)簡單的JS對象,并且最少包含標(biāo)簽名( tag)、屬性(attrs)和子元素對象( children)三個(gè)屬性。不同的框架對這三個(gè)屬性的命名會(huì)有點(diǎn)差別。
對于虛擬DOM,咱們來看一個(gè)簡單的實(shí)例,就是下圖所示的這個(gè),詳細(xì)的闡述了模板 → 渲染函數(shù) → 虛擬DOM樹 → 真實(shí)DOM的一個(gè)過程

其實(shí)虛擬DOM在Vue.js主要做了兩件事:
提供與真實(shí)DOM節(jié)點(diǎn)所對應(yīng)的虛擬節(jié)點(diǎn)vnode 將虛擬節(jié)點(diǎn)vnode和舊虛擬節(jié)點(diǎn)oldVnode進(jìn)行對比(diff算法),然后更新視圖
總結(jié):「vdom是為了減輕性能壓力。dom是昂貴的,昂貴的一方面在dom本身的重量,dom節(jié)點(diǎn)在js里的描述是個(gè)非常復(fù)雜屬性很多原型很長的超級對象,另一方面是瀏覽器渲染進(jìn)程和js進(jìn)程是獨(dú)立分離的,操作dom時(shí)的通信和瀏覽器本身需要重繪的時(shí)耗都是很高的。所以大家機(jī)智的搞了個(gè)輕量的vdom去模擬dom,vdom每個(gè)節(jié)點(diǎn)都只掛載js操作的必要屬性,每次組件update時(shí)都先操作vdom,通過vdom的比對,得到一個(gè)真實(shí)dom的需要操作的集合。整個(gè)機(jī)制是在JavaScript層面計(jì)算,在完成之前并不會(huì)操作DOM,等數(shù)據(jù)穩(wěn)定之后再實(shí)現(xiàn)精準(zhǔn)的修改?!?/strong>
分析diff算法
由上我們知道了,新的虛擬DOM和舊的虛擬DOm是通過diff算法進(jìn)行比較之后更新的。
Vue2.x diff算法
Vue2.x diff算法原理
傳統(tǒng)diff算法通過循環(huán)遞歸對節(jié)點(diǎn)進(jìn)行依次對比效率低下,算法復(fù)雜度達(dá)到O(N^3),主要原因在于其追求完全比對和最小修改,而React、Vue則是放棄了完全比對及最小修改,才實(shí)現(xiàn)從O(N^3) => O(N)。
優(yōu)化措施有:
「分層diff」:不考慮跨層級移動(dòng)節(jié)點(diǎn),讓新舊兩個(gè)VDOM樹的比對無需循環(huán)遞歸(復(fù)雜度大幅優(yōu)化,直接下降一個(gè)數(shù)量級的首要條件)。這個(gè)前提也是Web UI 中 DOM 節(jié)點(diǎn)跨層級的移動(dòng)操作特別少,可以忽略不計(jì)。
在同層節(jié)點(diǎn)中,采用了「雙端比較的算法」過程可以概括為:oldCh和newCh各有兩個(gè)頭尾的變量StartIdx和EndIdx,它們的2個(gè)變量相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設(shè)置了key,就會(huì)用key進(jìn)行比較,在比較的過程中,變量會(huì)往中間靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一個(gè)已經(jīng)遍歷完了,就會(huì)結(jié)束比較;

當(dāng)發(fā)生以下情況則跳過比對,變?yōu)椴迦牖騽h除操作:
「組件的Type(Tagname)不一致」,原因是絕大多數(shù)情況擁有相同type的兩個(gè)組件將會(huì)生成相似的樹形結(jié)構(gòu),擁有不同type的兩個(gè)組件將會(huì)生成不同的樹形結(jié)構(gòu),所以type不一致可以放棄繼續(xù)比對。
「列表組件的Key不一致」,舊樹中無新Key或反之。畢竟key是元素的身份id,能直接對應(yīng)上是否是同一個(gè)節(jié)點(diǎn)。
對觸發(fā)了getter/setter 的組件進(jìn)行diff,精準(zhǔn)減少diff范圍
Vue3.0 diff
diff痛點(diǎn)
vue2.x中的虛擬dom是進(jìn)行「全量的對比」,在運(yùn)行時(shí)會(huì)對所有節(jié)點(diǎn)生成一個(gè)虛擬節(jié)點(diǎn)樹,當(dāng)頁面數(shù)據(jù)發(fā)生變更好,會(huì)遍歷判斷virtual dom所有節(jié)點(diǎn)(包括一些不會(huì)變化的節(jié)點(diǎn))有沒有發(fā)生變化;雖然說diff算法確實(shí)減少了多DOM節(jié)點(diǎn)的直接操作,但是這個(gè)「減少是有成本的」,如果是復(fù)雜的大型項(xiàng)目,必然存在很復(fù)雜的父子關(guān)系的VNode,「而Vue2.x的diff算法,會(huì)不斷地遞歸調(diào)用 patchVNode,不斷堆疊而成的幾毫秒,最終就會(huì)造成 VNode 更新緩慢」。
那么Vue3.0是如何解決這些問題的呢
動(dòng)靜結(jié)合 PatchFlag
來個(gè)??:
<div>
<div>{msg}</div>
<div>靜態(tài)文字</div>
</div>
在Vue3.0中,在這個(gè)模版編譯時(shí),編譯器會(huì)在動(dòng)態(tài)標(biāo)簽?zāi)┪布由?/* Text*/ PatchFlag。「也就是在生成VNode的時(shí)候,同時(shí)打上標(biāo)記,在這個(gè)基礎(chǔ)上再進(jìn)行核心的diff算法」并且 PatchFlag 會(huì)標(biāo)識動(dòng)態(tài)的屬性類型有哪些,比如這里 的TEXT 表示只有節(jié)點(diǎn)中的文字是動(dòng)態(tài)的。而patchFlag的類型也很多。這里直接引用一張圖片。

其中大致可以分為兩類:
當(dāng) patchFlag 的值「大于」 0 時(shí),代表所對應(yīng)的元素在 patchVNode 時(shí)或 render 時(shí)是可以被優(yōu)化生成或更新的。
當(dāng) patchFlag 的值「小于」 0 時(shí),代表所對應(yīng)的元素在 patchVNode 時(shí),是需要被 full diff,即進(jìn)行遞歸遍歷 VNode tree 的比較更新過程。
看源碼:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
****
這里的_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)就是對變量節(jié)點(diǎn)進(jìn)行標(biāo)記。
總結(jié):「Vue3.0對于不參與更新的元素,做靜態(tài)標(biāo)記并提示,只會(huì)被創(chuàng)建一次,在渲染時(shí)直接復(fù)用。」
其中還有cacheHandlers(事件偵聽器緩存),這里就不講了。
diff算法源碼解析
以數(shù)組為栗子:
newNode:[a,b,c,d,e,f,g]
oldNode:[a,b,c,h,i,j,f,g]
步驟1:「從首部比較new vnode 和old vnode」,如果碰到不同的節(jié)點(diǎn),跳出循環(huán),否則繼續(xù),直到一方遍歷完成;
由此我們得到newNode和oldNode首部相同的片段為 a,b,c,
源碼:
const patchKeyedChildren = (
c1,
c2,
container,
parentAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
let i = 0;
const l2 = c2.length
let e1 = c1.length - 1
let e2 = c2.length - 1
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
parentAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
break
}
i++
}
//這里的isSameVNodeType
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
// 比較類型和key是否一致()
return n1.type === n2.type && n1.key === n2.key
}
「Tip」:這里的isSameVNodeType從「type和key」,因此key作為唯一值是非常重要的,這也就解釋了 v-for循環(huán)遍歷不能用index作為key的原因。
步驟2:「從尾部比較new vnode 和old vnode」,如果碰到不同的節(jié)點(diǎn),跳出循環(huán),否則繼續(xù),直到一方遍歷完成;
由此我們得到newNode和oldNode尾部相同的片段為 f,g
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
parentAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
break
}
e1--
e2--
}
}
在遍歷過程中滿足i > e1 && i < e2,說明 「僅有節(jié)點(diǎn)新增」
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos] : parentAnchor
while (i <= e2) {
patch(
null,
c2[i],
container,
anchor,
parentComponent,
parentSuspense,
isSVG
)
i++
}
}
} else if {
...
} else {
...
}
在遍歷過程中滿足i > e1 && i > e2,說明 「僅有節(jié)點(diǎn)移除」
if (i > e1) {
//
} else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
} else {
//
}
步驟3: 「節(jié)點(diǎn)移動(dòng)、新增或刪除」
經(jīng)過以上步驟,剩下的就是不確定的元素,那么diff算法將遍歷 所有的new node,將key和索引存在keyToNewIndexMap中,為map解構(gòu),
if (i > e1) {
//
} else if (i > e2) {
//
} else {
const s1 = i
const s2 = i
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = c2[i]
if (nextChild.key !== null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
}
接下來
for (i = s1; i <= e1; i++) { /* 開始遍歷老節(jié)點(diǎn) */
const prevChild = c1[i]
if (patched >= toBePatched) { /* 已經(jīng)patch數(shù)量大于等于, */
/* ① 如果 toBePatched新的節(jié)點(diǎn)數(shù)量為0 ,那么統(tǒng)一卸載老的節(jié)點(diǎn) */
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
/* ② 如果,老節(jié)點(diǎn)的key存在 ,通過key找到對應(yīng)的index */
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else { /* ③ 如果,老節(jié)點(diǎn)的key不存在 */
for (j = s2; j <= e2; j++) { /* 遍歷剩下的所有新節(jié)點(diǎn) */
if (
newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新節(jié)點(diǎn)沒有被patch */
isSameVNodeType(prevChild, c2[j] as VNode)
) { /* 如果找到與當(dāng)前老節(jié)點(diǎn)對應(yīng)的新節(jié)點(diǎn)那么 ,將新節(jié)點(diǎn)的索引,賦值給newIndex */
newIndex = j
break
}
}
}
if (newIndex === undefined) { /* ①?zèng)]有找到與老節(jié)點(diǎn)對應(yīng)的新節(jié)點(diǎn),刪除當(dāng)前節(jié)點(diǎn),卸載所有的節(jié)點(diǎn) */
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
/* ②把老節(jié)點(diǎn)的索引,記錄在存放新節(jié)點(diǎn)的數(shù)組中, */
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
/* 證明有節(jié)點(diǎn)已經(jīng)移動(dòng)了 */
moved = true
}
/* 找到新的節(jié)點(diǎn)進(jìn)行patch節(jié)點(diǎn) */
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
patched++
}
}
之后通過 計(jì)算出「最長遞增子序列」 減少Dom元素的移動(dòng),達(dá)到最少的 dom 操作。至于什么是最長遞增子序列,有興趣的小伙伴可以自信查閱。
最后
Vue3.0 的diff原理和優(yōu)化也講完了,如有錯(cuò)誤,歡迎指出。
