一文詳解Vue中的虛擬DOM與Diff算法
虛擬DOM
虛擬 DOM (Virtual DOM,簡稱 VDOM) 是一種編程概念,意為將目標(biāo)所需的 UI 通過數(shù)據(jù)結(jié)構(gòu)“虛擬”地表示出來,保存在內(nèi)存中,然后將真實(shí)的DOM
與之保持同步。具體來說,虛擬 DOM
是由一系列的 JavaScript 對(duì)象組成的樹狀結(jié)構(gòu),每個(gè)對(duì)象代表著一個(gè)DOM
元素,包括元素的標(biāo)簽名、屬性、子節(jié)點(diǎn)等信息。虛擬 DOM
中的每個(gè)節(jié)點(diǎn)都是一個(gè) JavaScript 對(duì)象,它們可以輕松地被創(chuàng)建、更新和銷毀,而不涉及到實(shí)際的DOM
操作。
主要作用
虛擬 DOM
的主要作用是在數(shù)據(jù)發(fā)生變化時(shí),通過與上一次渲染的虛擬 DOM
進(jìn)行對(duì)比,找出發(fā)生變化的部分,并最小化地更新實(shí)際 DOM
。這種方式可以減少實(shí)際 DOM
操作的次數(shù),從而提高頁面渲染的性能和效率。
總的來說,虛擬 DOM
是一種用 JavaScript 對(duì)象模擬真實(shí) DOM
結(jié)構(gòu)和狀態(tài)的技術(shù),它通過在內(nèi)存中操作虛擬 DOM 樹
來減少實(shí)際 DOM
操作,從而提高頁面的性能和用戶體驗(yàn)。
虛擬DOM樹
顧名思義,也就是一個(gè)虛擬 DOM 作為根節(jié)點(diǎn),包含有一個(gè)或多個(gè)的子虛擬 DOM。
Diff
在 Vue 3 中,diff(差異比較)是指在進(jìn)行虛擬 DOM 更新時(shí),對(duì)比新舊虛擬 DOM 樹的差異,然后只對(duì)實(shí)際發(fā)生變化的部分進(jìn)行更新,以盡可能地減少對(duì)真實(shí) DOM 的操作,提高頁面的性能和效率。diff
整體策略為:深度優(yōu)先,同層比較。也就是說,比較只會(huì)在同層級(jí)進(jìn)行, 不會(huì)跨層級(jí)比較;比較的過程中,循環(huán)從兩邊向中間收攏。
流程解析
Diff 算法的實(shí)現(xiàn)流程可以概括為以下幾個(gè)步驟:
比較根節(jié)點(diǎn): 首先,對(duì)比新舊虛擬 DOM 樹的根節(jié)點(diǎn),判斷它們是否相同。
逐層對(duì)比子節(jié)點(diǎn): 如果根節(jié)點(diǎn)相同,則逐層對(duì)比子節(jié)點(diǎn)。
比較子節(jié)點(diǎn)類型:
- 如果節(jié)點(diǎn)類型不同,則直接替換整個(gè)節(jié)點(diǎn)。
- 如果節(jié)點(diǎn)類型相同,繼續(xù)對(duì)比節(jié)點(diǎn)的屬性和事件。
對(duì)比子節(jié)點(diǎn)列表:
- 通過雙指針法對(duì)比新舊節(jié)點(diǎn)列表,查找相同位置的節(jié)點(diǎn)。
- 如果節(jié)點(diǎn)相同,進(jìn)行遞歸對(duì)比子節(jié)點(diǎn)。
- 如果節(jié)點(diǎn)不同,根據(jù)情況執(zhí)行插入、刪除或移動(dòng)節(jié)點(diǎn)的操作。
處理新增、刪除和移動(dòng)的節(jié)點(diǎn):
- 如果新節(jié)點(diǎn)列表中存在舊節(jié)點(diǎn)列表中沒有的節(jié)點(diǎn),執(zhí)行新增操作。
- 如果舊節(jié)點(diǎn)列表中存在新節(jié)點(diǎn)列表中沒有的節(jié)點(diǎn),執(zhí)行刪除操作。
- 如果新舊節(jié)點(diǎn)列表中都存在相同的節(jié)點(diǎn),但順序不同,執(zhí)行移動(dòng)節(jié)點(diǎn)的操作。
更新節(jié)點(diǎn)屬性和事件:
- 如果節(jié)點(diǎn)相同但屬性或事件發(fā)生了變化,更新節(jié)點(diǎn)的屬性和事件。
遞歸對(duì)比子節(jié)點(diǎn):
- 如果節(jié)點(diǎn)類型相同且是容器節(jié)點(diǎn)(例如 div、ul 等),則遞歸對(duì)比子節(jié)點(diǎn)。
源碼解析
在源碼中patchVnode是diff發(fā)生的地方,下面是patchVnode的源碼:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 如果新舊節(jié)點(diǎn)一致,什么都不做 if (oldVnode === vnode) { return } // 讓vnode.el引用到現(xiàn)在的真實(shí)dom,當(dāng)el修改時(shí),vnode.el會(huì)同步變化 const elm = vnode.elm = oldVnode.elm // 異步占位符 if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 如果新舊都是靜態(tài)節(jié)點(diǎn),并且具有相同的key // 當(dāng)vnode是克隆節(jié)點(diǎn)或是v-once指令控制的節(jié)點(diǎn)時(shí),只需要把oldVnode.elm和oldVnode.child都復(fù)制到vnode上 // 也不用再有其他操作 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 如果vnode不是文本節(jié)點(diǎn)或者注釋節(jié)點(diǎn) if (isUndef(vnode.text)) { // 并且都有子節(jié)點(diǎn) if (isDef(oldCh) && isDef(ch)) { // 并且子節(jié)點(diǎn)不完全一致,則調(diào)用updateChildren if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) // 如果只有新的vnode有子節(jié)點(diǎn) } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // elm已經(jīng)引用了老的dom節(jié)點(diǎn),在老的dom節(jié)點(diǎn)上添加子節(jié)點(diǎn) addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) // 如果新vnode沒有子節(jié)點(diǎn),而vnode有子節(jié)點(diǎn),直接刪除老的oldCh } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) // 如果老節(jié)點(diǎn)是文本節(jié)點(diǎn) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } // 如果新vnode和老vnode是文本節(jié)點(diǎn)或注釋節(jié)點(diǎn) // 但是vnode.text != oldVnode.text時(shí),只需要更新vnode.elm的文本內(nèi)容就可以 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
以上代碼主要就是用于比較新舊虛擬 DOM 節(jié)點(diǎn)并進(jìn)行更新。讓我逐步解釋這個(gè)函數(shù)的實(shí)現(xiàn):
- 判斷是否需要更新: 首先,函數(shù)會(huì)比較新舊虛擬 DOM 節(jié)點(diǎn)是否相同,如果相同則直接返回,無需進(jìn)行后續(xù)操作。
- 獲取舊節(jié)點(diǎn)的真實(shí) DOM 引用: 通過
elm = vnode.elm = oldVnode.elm
將新節(jié)點(diǎn)vnode
的真實(shí) DOM 引用指向舊節(jié)點(diǎn)的真實(shí) DOM。 - 處理異步占位符: 如果舊節(jié)點(diǎn)是異步占位符(
asyncPlaceholder
),并且新節(jié)點(diǎn)的異步工廠已經(jīng)解析,則通過hydrate
函數(shù)進(jìn)行同步操作;否則,將新節(jié)點(diǎn)標(biāo)記為異步占位符并返回。 - 處理靜態(tài)節(jié)點(diǎn): 如果新舊節(jié)點(diǎn)都是靜態(tài)節(jié)點(diǎn)(
isStatic
為真),并且具有相同的 key,則將新節(jié)點(diǎn)的組件實(shí)例引用指向舊節(jié)點(diǎn)的組件實(shí)例。 - 觸發(fā) prepatch 鉤子: 如果新節(jié)點(diǎn)的數(shù)據(jù)對(duì)象中定義了
hook
并且prepatch
鉤子存在,則執(zhí)行該鉤子函數(shù),用于預(yù)處理新舊節(jié)點(diǎn)之間的差異。 - 更新節(jié)點(diǎn)的屬性和事件: 如果新節(jié)點(diǎn)的數(shù)據(jù)對(duì)象中定義了
hook
并且update
鉤子存在,則執(zhí)行該鉤子函數(shù),用于更新節(jié)點(diǎn)的屬性和事件。 - 處理子節(jié)點(diǎn): 如果新舊節(jié)點(diǎn)都有子節(jié)點(diǎn),則比較它們之間的差異并進(jìn)行更新,調(diào)用
updateChildren
函數(shù)。如果只有新節(jié)點(diǎn)有子節(jié)點(diǎn),則將新節(jié)點(diǎn)的子節(jié)點(diǎn)添加到舊節(jié)點(diǎn)上。如果只有舊節(jié)點(diǎn)有子節(jié)點(diǎn),則刪除舊節(jié)點(diǎn)的子節(jié)點(diǎn)。如果舊節(jié)點(diǎn)是文本節(jié)點(diǎn),則清空其內(nèi)容。 - 更新文本內(nèi)容: 如果新舊節(jié)點(diǎn)都是文本節(jié)點(diǎn)或注釋節(jié)點(diǎn),并且它們的文本內(nèi)容不同,則更新新節(jié)點(diǎn)的文本內(nèi)容。
- 觸發(fā) postpatch 鉤子: 如果新節(jié)點(diǎn)的數(shù)據(jù)對(duì)象中定義了
hook
并且postpatch
鉤子存在,則執(zhí)行該鉤子函數(shù),用于處理節(jié)點(diǎn)更新后的操作。
Diff算法示例
下面是一個(gè)詳細(xì)的例子,假設(shè)有以下兩個(gè)虛擬 DOM 樹,我們將對(duì)它們進(jìn)行 diff 算法的比較:
舊的虛擬 DOM 樹:
{ type: 'div', props: { id: 'container' }, children: [ { type: 'p', props: { class: 'text' }, children: ['old Dom'] }, { type: 'button', props: { disabled: true }, children: ['click'] } ] }
新的虛擬 DOM 樹:
{ type: 'div', props: { id: 'container' }, children: [ { type: 'p', props: { class: 'text' }, children: ['new DOM'] }, { type: 'button', props: { disabled: false }, children: ['click'] }, { type: 'span', props: { class: 'msg' }, children: ['msg'] } ] }
Diff算法執(zhí)行:
比較根節(jié)點(diǎn):根節(jié)點(diǎn)相同,繼續(xù)比較子節(jié)點(diǎn)。
比較子節(jié)點(diǎn):
- 第一個(gè)子節(jié)點(diǎn)類型相同,但內(nèi)容不同,更新內(nèi)容為 'new DOM'。
- 第二個(gè)子節(jié)點(diǎn)相同,屬性發(fā)生變化,更新 disabled 屬性為 false。
- 第三個(gè)子節(jié)點(diǎn)是新增節(jié)點(diǎn),執(zhí)行插入操作。
更新節(jié)點(diǎn)屬性和事件:第二個(gè)子節(jié)點(diǎn)的屬性發(fā)生變化,更新 disabled 屬性。
遞歸對(duì)比子節(jié)點(diǎn):針對(duì)新增的 span 節(jié)點(diǎn),繼續(xù)遞歸對(duì)比其子節(jié)點(diǎn)。
最終結(jié)果:
{ type: 'div', props: { id: 'container' }, children: [ { type: 'p', props: { class: 'text' }, children: ['new DOM'] }, { type: 'button', props: { disabled: false }, children: ['click'] }, { type: 'span', props: { class: 'msg' }, children: ['msg'] } ] }
結(jié)語
總的來說,Diff 算法的核心思想是Diff就是將新老虛擬DOM的不同點(diǎn)找到并生成一個(gè)補(bǔ)丁,并根據(jù)這個(gè)補(bǔ)丁生成更新操作,以最小化對(duì)實(shí)際 DOM 的操作,提高頁面渲染的性能和效率。通過深度優(yōu)先、同層比較的策略,Diff 算法能夠高效地處理虛擬 DOM 樹的更新,使得頁面在數(shù)據(jù)變化時(shí)能夠快速響應(yīng)并更新對(duì)應(yīng)的視圖。
以上就是一文詳解Vue中的虛擬DOM與Diff算法的詳細(xì)內(nèi)容,更多關(guān)于Vue虛擬DOM與Diff算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
element日期時(shí)間選擇器限制時(shí)間選擇功能實(shí)現(xiàn)(精確到小時(shí))
文章介紹了如何使用Element UI的DateTimePicker組件來實(shí)現(xiàn)一個(gè)時(shí)間選擇器,該選擇器只能選擇當(dāng)前時(shí)間之后的7天,并且不能選擇當(dāng)前小時(shí),感興趣的朋友跟隨小編一起看看吧2025-01-01vue項(xiàng)目中canvas實(shí)現(xiàn)截圖功能
這篇文章主要為大家詳細(xì)介紹了vue項(xiàng)目中canvas實(shí)現(xiàn)截圖功能,截取圖片的一部分,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07vue使用高德地圖點(diǎn)擊下鉆上浮效果的實(shí)現(xiàn)思路
這篇文章主要介紹了vue使用高德地圖點(diǎn)擊下鉆 上浮效果的實(shí)現(xiàn)思路,本文以浙江省為例通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2019-10-10iview-table組件嵌套input?select數(shù)據(jù)無法雙向綁定解決
這篇文章主要為大家介紹了iview-table組件嵌套input?select數(shù)據(jù)無法雙向綁定解決示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09vue3+ts重復(fù)參數(shù)提取成方法多處調(diào)用以及字段無值時(shí)不傳字段給后端問題
在進(jìn)行API開發(fā)時(shí),優(yōu)化參數(shù)傳遞是一個(gè)重要的考量,傳統(tǒng)方法中,即使參數(shù)值為空,也會(huì)被包含在請(qǐng)求中發(fā)送給后端,這可能會(huì)導(dǎo)致不必要的數(shù)據(jù)處理,而優(yōu)化后的方法則只會(huì)傳遞那些實(shí)際有值的字段,從而提高數(shù)據(jù)傳輸?shù)挠行院秃蠖颂幚淼男?/div> 2024-10-10vue.js圖片轉(zhuǎn)Base64上傳圖片并預(yù)覽的實(shí)現(xiàn)方法
這篇文章主要介紹了vue.js圖片轉(zhuǎn)Base64上傳圖片并預(yù)覽的實(shí)現(xiàn)方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08詳解.vue文件中監(jiān)聽input輸入事件(oninput)
本篇文章主要介紹了詳解.vue文件中監(jiān)聽input輸入事件(oninput),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09詳解Vue如何實(shí)現(xiàn)響應(yīng)式布局
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)響應(yīng)式布局的兩種方法,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編一起了解一下2023-12-12最新評(píng)論