一文詳解Vue中的虛擬DOM與Diff算法
虛擬DOM
虛擬 DOM (Virtual DOM,簡(jiǎn)稱 VDOM) 是一種編程概念,意為將目標(biāo)所需的 UI 通過(guò)數(shù)據(jù)結(jié)構(gòu)“虛擬”地表示出來(lái),保存在內(nèi)存中,然后將真實(shí)的DOM
與之保持同步。具體來(lái)說(shuō),虛擬 DOM
是由一系列的 JavaScript 對(duì)象組成的樹(shù)狀結(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í),通過(guò)與上一次渲染的虛擬 DOM
進(jìn)行對(duì)比,找出發(fā)生變化的部分,并最小化地更新實(shí)際 DOM
。這種方式可以減少實(shí)際 DOM
操作的次數(shù),從而提高頁(yè)面渲染的性能和效率。
總的來(lái)說(shuō),虛擬 DOM
是一種用 JavaScript 對(duì)象模擬真實(shí) DOM
結(jié)構(gòu)和狀態(tài)的技術(shù),它通過(guò)在內(nèi)存中操作虛擬 DOM 樹(shù)
來(lái)減少實(shí)際 DOM
操作,從而提高頁(yè)面的性能和用戶體驗(yàn)。
虛擬DOM樹(shù)
顧名思義,也就是一個(gè)虛擬 DOM 作為根節(jié)點(diǎn),包含有一個(gè)或多個(gè)的子虛擬 DOM。
Diff
在 Vue 3 中,diff(差異比較)是指在進(jìn)行虛擬 DOM 更新時(shí),對(duì)比新舊虛擬 DOM 樹(shù)的差異,然后只對(duì)實(shí)際發(fā)生變化的部分進(jìn)行更新,以盡可能地減少對(duì)真實(shí) DOM 的操作,提高頁(yè)面的性能和效率。diff
整體策略為:深度優(yōu)先,同層比較。也就是說(shuō),比較只會(huì)在同層級(jí)進(jìn)行, 不會(huì)跨層級(jí)比較;比較的過(guò)程中,循環(huán)從兩邊向中間收攏。
流程解析
Diff 算法的實(shí)現(xiàn)流程可以概括為以下幾個(gè)步驟:
比較根節(jié)點(diǎn): 首先,對(duì)比新舊虛擬 DOM 樹(shù)的根節(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)列表:
- 通過(guò)雙指針?lè)▽?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)列表中沒(méi)有的節(jié)點(diǎn),執(zhí)行新增操作。
- 如果舊節(jié)點(diǎn)列表中存在新節(jié)點(diǎn)列表中沒(méi)有的節(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沒(méi)有子節(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)是否相同,如果相同則直接返回,無(wú)需進(jìn)行后續(xù)操作。
- 獲取舊節(jié)點(diǎn)的真實(shí) DOM 引用: 通過(guò)
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)解析,則通過(guò)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 樹(shù),我們將對(duì)它們進(jìn)行 diff 算法的比較:
舊的虛擬 DOM 樹(shù):
{ type: 'div', props: { id: 'container' }, children: [ { type: 'p', props: { class: 'text' }, children: ['old Dom'] }, { type: 'button', props: { disabled: true }, children: ['click'] } ] }
新的虛擬 DOM 樹(shù):
{ 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é)語(yǔ)
總的來(lái)說(shuō),Diff 算法的核心思想是Diff就是將新老虛擬DOM的不同點(diǎn)找到并生成一個(gè)補(bǔ)丁,并根據(jù)這個(gè)補(bǔ)丁生成更新操作,以最小化對(duì)實(shí)際 DOM 的操作,提高頁(yè)面渲染的性能和效率。通過(guò)深度優(yōu)先、同層比較的策略,Diff 算法能夠高效地處理虛擬 DOM 樹(shù)的更新,使得頁(yè)面在數(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組件來(lái)實(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)思路,本文以浙江省為例通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2019-10-10iview-table組件嵌套input?select數(shù)據(jù)無(wú)法雙向綁定解決
這篇文章主要為大家介紹了iview-table組件嵌套input?select數(shù)據(jù)無(wú)法雙向綁定解決示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09vue3+ts重復(fù)參數(shù)提取成方法多處調(diào)用以及字段無(wú)值時(shí)不傳字段給后端問(wèn)題
在進(jìn)行API開(kāi)發(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)方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08詳解.vue文件中監(jiān)聽(tīng)input輸入事件(oninput)
本篇文章主要介紹了詳解.vue文件中監(jiān)聽(tīng)input輸入事件(oninput),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧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)論