您好,登錄后才能下訂單哦!
這篇文章主要介紹了Vue中的虛擬DOM和Diff算法實例分析的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Vue中的虛擬DOM和Diff算法實例分析文章都會有所收獲,下面我們一起來看看吧。
先用一個簡單的例子來說一下 虛擬DOM
和 diff算法
:比如有一個戶型圖,現在我們需要對這個戶型圖進行下面的改造,
其實,這個就是相當于一個進行找茬的游戲,讓我們找出與原來的不同之處。下面,我已經將不同之處圈了出來,
現在,我們已經知道了要進行哪些改造了,但是,我們該如何進行改造呢?最笨的方法就是全部拆了再重新建一次,但是,在我們實際中肯定不會進行拆除再新建,這樣效率太低了,而且代價太昂貴。確實是完成了改造,但是,這不是一個最小量的更新,所以我們想要的是 diff,
那么diff是什么呢?其實,diff
在我們計算機中就是代表著最小量更新的一個算法,會進行精細化對比,以最小量去更新。這樣你就會發現,它的代價比較小,也不會昂貴,也會比較優化,所以對應在我們 Vue底層中是非常關鍵的。
好了,現在回歸到我們的Vue中,上面的戶型圖中就相當于vue中的 DOM節點,我們需要對這些節點進行改造(增刪調),然后以最小量去更新DOM
,這樣就會避免我們性能上面的開銷。
// 原先DOM <div class="box"> <h3>標題</h3> <ul> <li>1</li> <li>2</li> <li>3</li> </ul> </div>
// 修改后的DOM <div class="box"> <h3>標題</h3> <span>青峰</span> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> </div>
在這里,我們就可以利用 diff算法進行精細化對比,實現最小量更新
。
上面我們了解了什么是diff,下面再來簡單了解一下什么是虛擬DOM,
<div class="box"> <h3>標題</h3> <ul> <li>1</li> <li>2</li> <li>3</li> </ul> </div>
{ sel: "div", elm: undefined, // 表示虛擬節點還沒有上樹 key: undefined, // 唯一標識 data: { class: { "box" : true} }, children: [ { sel: "h3", data: {}, text: "標題" }, { sel: "ul", data: {}, children: [ { sel: li, data: {}, text: "1"}, { sel: li, data: {}, text: "2"}, { sel: li, data: {}, text: "3"} ] } ] }
通過觀察可以發現,虛擬DOM
是一個 JavsScript對象
,里面包含 sel選擇器
,data數據
,text文本內容
,children子標簽
等等,一層嵌套一層。這樣就表達了一個 虛擬DOM結構
,處理 虛擬DOM 的方式總比處理 真實的DOM 要簡單并且高效,所以 diff算法
是發生在 虛擬DOM
上的。
注意:diff算法 是發生在 虛擬DOM 上的。
首先,我們都知道,在前端性能優化的一個秘訣就是盡可能的減少DOM的操作
,不僅僅是DOM相對較慢,更是因為變動DOM會造成瀏覽器的回流和重繪
,這些都會降低性能,因此,我們需要虛擬DOM,在patch(比較新舊虛擬DOM更新去更新視圖)
過程中盡可能的一次性將差異更新到DOM中
,這樣就保證了DOM不會出現了性能很差的情況。
其次,使用 虛擬DOM
改變了當前的狀態不需要立即去更新DOM,而是對更新的內容進行更新,對于沒有改變的內容不做任何處理,通過前后兩次差異進行比較
。
最后,也是Virtual DOM 最初的目的,就是更好的跨平臺
,比如node.js就沒有DOM,如果想實現 SSR(服務端渲染)
,那么一個方式就是借助Virtual DOM,因為 Virtual DOM本身是 JavaScript對象。
作用:h函數 主要用來產生 虛擬節點(vnode)
第一個參數:標簽名字、組件的選項對象、函數
第二個參數:標簽對應的屬性 (可選)
第三個參數:子級虛擬節點,字符串或者是數組形式
h('a',{ props: {href: 'http://www.baidu.com'}, '百度'})
上面的h函數對應的虛擬節點為:
{ sel: 'a', data: { props: {href: 'http://www.baidu.com'}}, text: "百度"}
真正的DOM節點為:
<a href = "http://www.baidu.com">百度</a>
我們還可以嵌套的使用h函數,比如:
h('ul', {}, [ h('li', {}, '1'), h('li', {}, '2'), h('li', {}, '3'), ])
嵌套使用h函數,就會生成一個虛擬DOM樹。
{ sel: "ul", elm: undefined, key: undefined, data: {}, children: [ { sel: li, elm: undefined, key: undefined, data: {}, text: "1"}, { sel: li, elm: undefined, key: undefined, data: {}, text: "2"}, { sel: li, elm: undefined, key: undefined, data: {}, text: "3"} ] }
好了,上面我們已經知道了h函數是怎么使用的了,下面我們手寫一個閹割版的h函數。
我們手寫的這個函數只考慮三種情況(帶三個參數),分別如下:
情況①:h('div', {}, '文字') 情況②:h('div', {}, []) 情況③:h('div', {}, h())
在手寫h函數之前,我們需要聲明一個函數,用來創建虛擬節點
// vnode.js 返回虛擬節點 export default function(sel, data, children, text, elm) { // sel 選擇器 、data 屬性、children 子節點、text 文本內容、elm 虛擬節點綁定的真實 DOM 節點 const key = data.key return { sel, data, children, text, elm, key } }
聲明好vnode函數之后,我們正式來手寫h函數,思路如下:
判斷第三個參數是否是字符串或者是數字
。如果是字符串或數字,直接返回 vnode
判斷第三個參數是否是一個數組
。聲明一個數組
,用來存儲子節點
,需要遍歷數組,這里需要判斷每一項是否是一個對象(因為 vnode 返回一個對象并且一定會有sel屬性)但是不需要執行每一項,因為在數組里面已經執行了h函數。其實,并不是函數遞歸進行調用(自己調自己),而是一層一層的嵌套
判斷都三個參數是否是一個對象
。直接將這個對象賦值給 children
,并會返回 vnode
// h.js h函數 import vnode from "./vnode"; // 情況①:h('div', {}, '文字') // 情況②:h('div', {}, []) // 情況③:h('div', {}, h()) export default function (sel, data, c) { // 判斷是否傳入三個參數 if (arguments.length !== 3) throw Error('傳入的參數必須是三個參數') // 判斷c的類型 if (typeof c === 'string' || typeof c === 'number') { // 情況① return vnode(sel, data, undefined, c, undefined) } else if(Array.isArray(c)) { // 情況② // 遍歷 let children = [] for(let i = 0; i < c.length; i++) { // 子元素必須是h函數 if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) throw Error('數組中有一項不是h函數') // 收集子節點 不需要執行 因為數組里面已經執行h函數來 children.push(c[i]) } return vnode(sel, data, children, undefined, undefined) } else if (typeof c === 'object' && c.hasOwnProperty('sel')) { // 直接將子節點放到children中 let children = [c] return vnode(sel, data, children, undefined, undefined) } else { throw Error('傳入的參數格式不對') } }
通過上面的代碼,我們已經實現了一個簡單 h函數
的基本功能。
在講解 diff算法 之前,我們先來感受一下 diff算法 的強大之處。先利用 snabbdom 簡單來舉一個例子。
import { init, classModule, propsModule, styleModule, eventListenersModule, h, } from "snabbdom"; //創建出patch函數 const patch = init([ classModule, propsModule, styleModule, eventListenersModule, ]); //讓虛擬節點上樹 const container = document.getElementById("container"); const btn = document.getElementById("btn"); //創建虛擬節點 const myVnode1 = h('ul', {}, [ h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, 'C'), h('li', {}, 'D') ]) patch(container, myVnode1) const myVnode2 = h('ul', {}, [ h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, 'C'), h('li', {}, 'D'), h('li', {}, 'E'), ]) btn.addEventListener('click', () => { // 上樹 patch(myVnode1,myVnode2) })
當我們點接改變DOM的時候,發現會新增一個 li標簽 內容為 E,單單的點擊事件,我們很難看出,是將 舊的虛擬DOM
全部替換掉 新的虛擬DOM
,然后再渲染成 真實DOM,還是直接在 舊的虛擬DOM
上直接在后面添加一個節點
,所以,在這里我們可以巧妙的打開測試工具,直接將標簽內容進行修改,如果點擊之后是全部拆除,那么標簽的內容就會發生改變,若內容沒有發生改變,則是將最后添加的。
點擊改變 DOM 結構:
果然,之前修改的內容沒有發生變化,這一點,就可以驗證了是進行了 diff算法精細化的比較,以最小量進行更新
。
那么問題就來了,如果我在前面添加一個節點呢?是不是也是像在最后添加一樣,直接在前面添加一個節點。我們不妨也來試一試看看效果:
... const container = document.getElementById("container"); const btn = document.getElementById("btn"); //創建虛擬節點 const myVnode1 = h('ul', {}, [ h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, 'C'), h('li', {}, 'D') ]) patch(container, myVnode1) const myVnode2 = h('ul', {}, [ h('li', {}, 'E'), // 將E移至前面 h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, 'C'), h('li', {}, 'D'), ]) btn.addEventListener('click', () => { // 上樹 patch(myVnode1,myVnode2) })
點擊改變 DOM 結構
哦豁!!跟我們想的不一樣,你會發現,里面的文本內容全部發生了變化,也就是說將之前的 DOM 全部拆除,然后將新的重新上樹。這時候,你是不是在懷疑其實 diff算法 沒有那么強大,但是你這樣想就大錯特錯了,回想一下在學習 Vue 的過程中,在遍歷DOM節點 的時候,是不是特別的強調要寫上key唯一標識符
,此時,key在這里就發揮了它的作用。 我們帶上key再來看一下效果:
... const myVnode1 = h('ul', {}, [ h('li', { key: "A" }, 'A'), h('li', { key: "B" }, 'B'), h('li', { key: "C" }, 'C'), h('li', { key: "D" }, 'D') ]) patch(container, myVnode1) const myVnode2 = h('ul', {}, [ h('li', { key: "E" }, 'E'), h('li', { key: "A" }, 'A'), h('li', { key: "B" }, 'B'), h('li', { key: "C" }, 'C'), h('li', { key: "D" }, 'D'), ]) ...
點擊改變 DOM 結構
看到上面的結果,此時此刻,你是不是恍然大悟了,頓時知道了key在循環當中有什么作用了吧。我們可以推出的結論一就是:
key是當前節點的唯一標識,告訴 diff算法
,在更改前后它們是同一個 DOM節點
。
當我們修改父節點,此時新舊虛擬DOM的父節點不是同一個節點,繼續來觀察一下 diff算法是如何分析的
const myVnode1 = h('ul', {}, [ h('li', { key: "A" }, 'A'), h('li', { key: "B" }, 'B'), h('li', { key: "C" }, 'C'), h('li', { key: "D" }, 'D') ]) patch(container, myVnode1) const myVnode2 = h('ol', {}, [ h('li', { key: "A" }, 'A'), h('li', { key: "B" }, 'B'), h('li', { key: "C" }, 'C'), h('li', { key: "D" }, 'D'), ])
點接改變 DOM結構
你會發現,這里將舊節點進行了全部的拆除,然后重新將新節點上樹。我們可以推出的結論二就是:
只有是同一個虛擬節點
,diff算法
才進行精細化比較,否則就是暴力刪除舊的、插入新的。判斷同一個虛擬節點的依據:選擇器(sel)相同且key相同。
那么如果是同一個虛擬節點,但是子節點里面不是同一層在比較的呢?
const myVnode1 = h('div', {}, [ h('li', { key: "A" }, 'A'), h('li', { key: "B" }, 'B'), h('li', { key: "C" }, 'C'), h('li', { key: "D" }, 'D') ]) patch(container, myVnode1) const myVnode2 = h('div', {}, h('section', {}, [ h('li', { key: "A" }, 'A'), h('li', { key: "B" }, 'B'), h('li', { key: "C" }, 'C'), h('li', { key: "D" }, 'D'), ] ))
點擊改變DOM結構
你會發現,此時DOM結構同多了一層 section標簽 包裹著,然后,文本的內容也發生了變化,所以我們可以推出結論三:diff算法
只進行同層比較,不會進行跨層比較
。即使是同一片虛擬節點,但是跨層了,不進行精細化比較,而是暴力刪除舊的、然后插入新的。
綜上,我們得出diff算法的三個結論:
key
是當前節點的唯一標識,告訴 diff算法
,在更改前后它們是同一個 DOM節點
。
只有是同一個虛擬節點
,diff算法
才進行精細化比較,否則就是暴力刪除舊的、插入新的。判斷同一個虛擬節點的依據:選擇器(sel)相同
且 key相同
。
diff算法
只進行同層比較,不會進行跨層比較
。即使是同一片虛擬節點,但是跨層了,不進行精細化比較,而是暴力刪除舊的、然后插入新的。
看到這里,相信你已經對 diff算法 已經有了很大的收獲了。
patch函數 的主要作用就是:判斷是否是同一個節點類型,是就在進行精細化對比,不是就進行暴力刪除,插入新的
。
我們在可以簡單的畫出patch函數現在的主要流程圖如下:
// patch.js patch函數 import vnode from "./vnode"; import sameVnode from "./sameVnode"; import createElement from "./createElement"; export default function (oldVnode, newVnode) { // 判斷oldVnode是否是虛擬節點 if (oldVnode.sel == '' || oldVnode.sel == undefined) { // console.log('不是虛擬節點'); // 創建虛擬DOM oldVnode = emptyNodeAt(oldVnode) } // 判斷是否是同一個節點 if (sameNode(oldVnode, newVnode)) { console.log('是同一個節點'); } else { // 暴力刪除舊節點,插入新的節點 // 傳入兩個參數,創建的節點 插入到指定標桿的位置 createElement(newVnode, oldVnode.elm) } } // 創建虛擬DOM function emptyNodeAt (elm) { return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm) }
在進行上DOM上樹之前,我們需要了解一下DOM中的insertBefore()方法、appendChild()方法,因為,只有你真正的知道它們兩者的用法,才會讓你在下面手寫上樹的時候更加的清晰。
appendChild()方法
appendChild() 方法
:可以向節點的子節點列表的末尾添加新的子節點。比如:appendChild(newchild)。
注意: appendChild()方法是在父節點中的子節點的末尾
添加新的節點。(相對于父節點來說)。
<body> <div class="box"> <span>青峰</span> <ul> <li>1</li> <li>2</li> </ul> </div> <script> const box = document.querySelector('.box') const appendDom = document.createElement('div') appendDom.style.backgroundColor = 'blue' appendDom.style.height = 100 + 'px' appendDom.style.width = 100 + 'px' // 在box里面的末尾追加一個div box.appendChild(appendDom) </script> </body>
你會發現,創建的div是嵌套在box里面的,div 屬于 box 的子節點,box 是 div 的子節點。
insertBefore()方法
insertBefore() 方法
:可在已有的字節點前中插入一個新的子節點。比如:insertBefore(newchild,rechild)。
注意: insertBefore()方法是在已有的節點前
添加新的節點。(相對于子節點來說的)。
<body> <div class="box"> <span>青峰</span> <ul> <li>1</li> <li>2</li> </ul> </div> <script> const box = document.querySelector('.box') const insertDom = document.createElement('p') insertDom.innerText = '我是insertDOM' // 在body中 box前面添加新的節點 box.parentNode.insertBefore(insertDom, box) </script> </body>
我們發現,box 和 div 是同一層的,屬于兄弟節點。
處理不同節點
sameVnode 函數
作用:比較兩個節點是否是同一個節點
// sameVnode.js export default function sameVnode(oldVnode, newVnode) { return (oldVnode.data ? oldVnode.data.key : undefined) === (newVnode.data ? newVnode.data.key : undefined) && oldVnode.sel == newVnode.sel }
手寫第一次上樹
理解了上面的 appendChild()方法
、insertBefore()方法
之后,我們正式開始讓 真實DOM 上樹
,渲染頁面。
// patch.js patch函數 import vnode from "./vnode"; import sameVnode from "./sameVnode"; import createElement from "./createElement"; export default function (oldVnode, newVnode) { // 判斷oldVnode是否是虛擬節點 if (oldVnode.sel == '' || oldVnode.sel == undefined) { // console.log('不是虛擬節點'); // 創建虛擬DOM oldVnode = emptyNodeAt(oldVnode) } // 判斷是否是同一個節點 if (sameNode(oldVnode, newVnode)) { console.log('是同一個節點'); } else { // 暴力刪除舊節點,插入新的節點 // 傳入兩個參數,創建的節點 插入到指定標桿的位置 createElement(newVnode, oldVnode.elm) } } // 創建虛擬DOM function emptyNodeAt (elm) { return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm) }
上面我們已經明確的知道,patch的作用就是判斷是否是同一個節點,所以,我們需要聲明一個createElement函數,用來創建真實DOM。
createElement 函數
createElement主要用來 創建子節點的真實DOM。
// createElement.js export default function createElement(vnode,pivot) { // 創建上樹的節點 let domNode = document.createElement(vnode.sel) // 判斷有文本內容還是子節點 if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) { // 文本內容 直接賦值 domNode.innerText = vnode.text // 上樹 往body上添加節點 // insertBefore() 方法:可在已有的字節點前中插入一個新的子節點。相對于子節點來說的 pivot.parentNode.insertBefore(domNode, pivot) } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { // 有子節點 } }
// index.js import patch from "./mysnabbdom/patch"; import h from './mysnabbdom/h' const container = document.getElementById("container"); //創建虛擬節點 const myVnode1 = h('h2', {}, '文字') patch(container, myVnode1)
我們已經將創建的真實DOM成功的渲染到頁面上去了,但是這只是實現了最簡單的一種情況,那就是 h函數 第三個參數是字符串的情況,所以,當第三個參數是一個數組的時候,是無法進行上樹的,下面我們需要將 createElement函數 再進一步的優化,實現遞歸上樹。
遞歸創建子節點
我們發現,在第一次上樹的時候,createElement函數
有兩個參數,分別是:newVnode
(新的虛擬DOM),標桿
(用來上樹插入到某個節點的位置),在createElement內部
我們是使用 insertBefore()方法
進行上樹的,使用這個方法我們需要知道已有的節點是哪一個,當然,當有 text
(第三個參數是字符串或數字)的時候,我們是可以找到要插入的位置的,但是當有 children
(子節點)的時候,我們是無法確定標桿的位置的,所以,我們要將上樹的工作放到 patch函數
中,即 createElement函數
就只負責創建節點
。
// index.js import patch from "./mysnabbdom/patch"; import h from './mysnabbdom/h' const container = document.getElementById("container"); //創建虛擬節點 const myVnode1 = h('ul', {}, [ h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, 'C'), h('li', {}, 'D') ]) patch(container, myVnode1)
// patch.js import vnode from "./vnode"; import sameVnode from "./sameVnode"; import createElement from "./createElement"; export default function (oldVnode, newVnode) { // 判斷oldVnode是否是虛擬節點 if (oldVnode.sel == '' || oldVnode.sel == undefined) { // console.log('不是虛擬節點'); // 創建虛擬DOM oldVnode = emptyNodeAt(oldVnode) } // 判斷是否是同一個節點 if (sameNode(oldVnode, newVnode)) { console.log('是同一個節點'); } else { // 暴力刪除舊節點,插入新的節點 // 傳入參數為創建的虛擬DOM節點 返回以一個真實DOM let newVnodeElm = createElement(newVnode) console.log(newVnodeElm); // oldVnode.elm.parentNode 為body 在body中 在舊節點的前面添加新的節點 if (oldVnode.elm.parentNode && oldVnode.elm) { oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm) } // 刪除老節點 oldVnode.elm.parentNode.removeChild(oldVnode.elm) } } // 創建虛擬DOM function emptyNodeAt (elm) { return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm) }
完善 createElement 函數
// createElement.js只負責創建真正節點 export default function createElement(vnode) { // 創建上樹的節點 let domNode = document.createElement(vnode.sel) // 判斷有文本內容還是子節點 if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) { // 文本內容 直接賦值 domNode.innerText = vnode.text // 上樹 往body上添加節點 // insertBefore() 方法:可在已有的字節點前中插入一個新的子節點。相對于子節點來說的 } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { // 有子節點 for(let i = 0; i < vnode.children.length; i++) { // console.log(vnode.children[i]); let ch = vnode.children[i] // 進行遞歸 一旦調用createElement意味著 創建了DOM 并且elm屬性指向了創建好的DOM let chDom = createElement(ch) // 添加節點 使用appendChild 因為遍歷下一個之前 上一個真實DOM(這里的domVnode)已經生成了 所以可以使用appendChild domNode.appendChild(chDom) } } vnode.elm = domNode return vnode.elm }
經過上面的分析,我們已經完成了對createElem函數的完善,可能你對這個遞歸有點不了解,那么大概捋一下進行的過程:
首先,一開始的這個 新的虛擬DOM的sel
屬性為 ul,創建的真實DOM節點為 ul,執行 createElement函數
發現,新的虛擬DOM里面有children屬性
,children 屬性里面又包含 h函數。
其次,進入到for循環中,拿到 children 中的第一項,然后再次 調用crateElement函數
創建真實DOM
,上面第一次調用createElement的時候已經創建了ul,執行完第一項返回創建的虛擬DOM,然后使用 appendChild方法()
追加到 ul中,依次類推,執行后面的數組項。
最后,將創建好的 所有真實DOM
返回出去,在 patch函數
中上樹。
執行上面的代碼,測試結果如下:
完美!我們成功的將遞歸子節點完成了,無論嵌套多少層,我們都可以通過遞歸將子節點渲染到頁面上。
前面,我們實現了不是同一個節點的時候,進行刪除舊節點和插入新節點的操作,下面,我們來實現是相同節點時的相關操作,這也是文章中最重要的部分,diff算法
就包含在其中!!!
處理相同節點
上面的 patch函數
流程圖中,我們已經處理了不同節點的時候,進行暴力刪除舊的節點,然后插入新的節點,現在我們進行處理相同節點的時候,進行精細化的比較,繼續完善 patch函數 的主流程圖:
看到上面的流程圖,你可能會有點疑惑,為什么不在 newVnode 是否有 Text屬性 中繼續判斷 oldVnode 是否有 children 屬性而是直接判斷兩者之間的 Text 是否相同,這里需要提及一個知識點
,當我們進行 DOM操作的時候,文本內容替換DOM的時候,會自動將DOM結構全部銷毀掉
,innerText改變了,DOM結構也會隨之被銷毀,所以這里可以不用判斷 oldVnode 是否存在 children 屬性,如果插入DOM節點,此時的Text內容并不會被銷毀掉,所以我們需要手動的刪除。
這也是為什么在流程圖后面,我們添加 newVnode 的children 的時候需要將 oldVnode 的 Text 手動刪除,而將 newVnode 的 Text 直接賦值
給oldVnode.elm.innerText
的原因。
知道上面流程圖是如何工作了,我們繼續來書寫patch函數中是同一個節點的代碼。
// patch.js import vnode from "./vnode"; import sameVnode from "./sameVnode"; import createElement from "./createElement"; export default function (oldVnode, newVnode) { // 判斷oldVnode是否是虛擬節點 if (oldVnode.sel == '' || oldVnode.sel == undefined) { // console.log('不是虛擬節點'); // 創建虛擬DOM oldVnode = emptyNodeAt(oldVnode) } // 判斷是否是同一個節點 if (sameNode(oldVnode, newVnode)) { console.log('是同一個節點'); // 是否是同一個對象 if (oldVnode === newVnode) return // newVnode是否有text if (newVnode.text && (newVnode.children == undefined || newVnode.children.length == 0)) { // 判斷newVnode和oldVnode的text是否相同 if (!(newVnode.text === oldVnode.text)) { // 直接將text賦值給oldVnode.elm.innerText 這里會自動銷毀oldVnode的cjildred的DOM結構 oldVnode.elm.innerText = newVnode.text } // 意味著newVnode有children } else { // oldVnode是否有children屬性 if (oldVnode.children != undefined && oldVnode.children.length > 0) { // oldVnode有children屬性 } else { // oldVnode沒有children屬性 // 手動刪除 oldVnode的text oldVnode.elm.innerText = '' // 遍歷 for (let i = 0; i < newVnode.children.length; i++) { let dom = createElement(newVnode.children[i]) // 追加到oldvnode.elm中 oldVnode.elm.appendChild(dom) } } } } else { // 暴力刪除舊節點,插入新的節點 // 傳入參數為創建的虛擬DOM節點 返回以一個真實DOM let newVnodeElm = createElement(newVnode) console.log(newVnodeElm); // oldVnode.elm.parentNode 為body 在body中 在舊節點的前面添加新的節點 if (oldVnode.elm.parentNode && oldVnode.elm) { oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm) } // 刪除老節點 oldVnode.elm.parentNode.removeChild(oldVnode.elm) } } // 創建虛擬DOM function emptyNodeAt(elm) { return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm) }
.... //創建虛擬節點 const myVnode1 = h('ul', {}, 'oldVnode有text') patch(container, myVnode1) const myVnode2 = h('ul', {}, [ h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, 'C'), h('li', {}, 'D') ]) btn.addEventListener('click', () => { patch(myVnode1, myVnode2) })
oldVnode 有 tex屬性 和 newVnode 有 children屬性 的效果如下:
... //創建虛擬節點 const myVnode1 = h('ul', {}, [ h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, 'C'), h('li', {}, 'D') ]) patch(container, myVnode1) const myVnode2 = h('ul', {}, 'newVnode 有text') btn.addEventListener('click', () => { patch(myVnode1, myVnode2) })
oldVode 有children屬性 和 newVnode 有 text屬性 的效果如下:
完美!現在我們就只差最后diff算了。
patchVnode 函數
在patch函數中,我們需要將同同一節點的比較分成一個單獨的模塊patchVnode函數,方便我們在diff算法中進行遞歸運算。
patchVnode函數的主要作用就是:
判斷newVnode
和oldVnode
是否指向同一個對象,如果是,那么直接return
如果他們都有text并且不相等 或者 oldVnode
有子節點而newVnode
沒有,那么將oldVnode.elm
的文本節點設置為newVnode
的文本節點。
如果oldVnode
沒有子節點而newVnode
有,則將newVnode
的子節點真實化之后添加到oldVnode.elm
后面,然后刪除 oldVnode.elm
的 text
如果兩者都有子節點,則執行updateChildren
函數比較子節點,這一步很重要
// patchVnode.js export default function patchVnode(oldVnode, newVnode) { // 是否是同一個對象 if (oldVnode === newVnode) return // newVnode是否有text if (newVnode.text && (newVnode.children == undefined || newVnode.children.length == 0)) { // 判斷newVnode和oldVnode的text是否相同 if (!(newVnode.text === oldVnode.text)) { // 直接將text賦值給oldVnode.elm.innerText 這里會自動銷毀oldVnode的cjildred的DOM結構 oldVnode.elm.innerText = newVnode.text } //說明 newVnode有 children } else { // oldVnode是否有children屬性 if (oldVnode.children != undefined && oldVnode.children.length > 0) { // oldVnode有children屬性 } else { // oldVnode沒有children屬性 // 手動刪除 oldVnode的text oldVnode.elm.innerText = '' // 遍歷 for (let i = 0; i < newVnode.children.length; i++) { let dom = createElement(newVnode.children[i]) // 追加到oldvnode.elm中 oldVnode.elm.appendChild(dom) } } } }
diff算法
精細化比較:diff算法 四種優化策略
這里使用雙指針
的形式進行diff算法的比較,分別是舊前、舊后、新前、新后指針,(前指針往下移動,后指針往上移動
)
四種優化策略:(命中:key 和 sel 都要相同)
①、新前與舊前
②、新后與舊后
③、新后與舊前
④、新前與舊后
注意: 當只有第一種不命中的時候才會采取第二種,依次類推,如果四種都不命中,則需要通過循環
來查找。
命中指針才會移動,否則不移動。
①、新前與舊前
如果就舊節點先循環完畢,說明需要新節點中有需要插入的節點
。
②、新后與舊后
如果新節點先循環完畢,舊節點還有剩余節點,說明舊節點中有需要刪除的節點。
多刪除情況:當只有情況①命中,剩下三種都沒有命中,則需要進行循環遍歷,找到舊節點中對應的節點
,然后在舊的虛擬節點中將這個節點設置為undefined
。刪除的節點為舊前與舊后之間(包含舊前、舊后)。
③、新后與舊前
當③新后與舊前命中
的時候,此時要移動節點,移動 新后指向的節點
到舊節點的 舊后的后面
,并且找到舊節點中對應的節點
,然后在舊的虛擬節點中將這個節點設置為undefined
。
④、新前與舊后
當④新前與舊后
命中的時候,此時要移動節點,移動新前
指向的這個節點到舊節點的 舊前的前面
,并且找到舊節點中對應的節點
,然后在舊的虛擬節點中將這個節點設置為undefined
。
好了,上面通過動態講解的四種命中方式之后,動態gif圖片有水印,看著可能不是很舒服,但當然能夠理解是最重要的,那么我們開始手寫 diff算法 的代碼。
updateChildren 函數
updateChildren()方法
主要作用就是進行精細化比較,然后更新子節點
。這里代碼比較多,需要耐心的閱讀。
import createElement from "./createElement"; import patchVnode from "./patchVnode"; import sameVnode from "./sameVnode"; export default function updateChildren(parentElm, oldCh, newCh) { //parentElm 父節點位置 用來移動節點 oldCh舊節點children newCh新節點children // console.log(parentElm, oldCh, newCh); // 舊前 let oldStartIndex = 0 // 舊后 let oldEndIndex = oldCh.length - 1 // 新前 let newStartIndex = 0 // 舊后 let newEndIndex = newCh.length - 1 // 舊前節點 let oldStartVnode = oldCh[0] // 舊后節點 let oldEndVnode = oldCh[oldEndIndex] // 新前節點 let newStartVnode = newCh[0] // 新后節點 let newEndVnode = newCh[newEndIndex] // 存儲mapkey let keyMap // 循環 條件 舊前 <= 舊后 && 新前 <= 新后 while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 首先需要判斷是否已經處理過了 if (oldCh[oldStartIndex] == undefined) { oldStartVnode = oldCh[++oldStartIndex] } else if (oldCh[oldStartIndex] == undefined) { oldEndVnode = oldCh[--oldEndIndex] } else if (newCh[newStartIndex] == undefined) { newStartVnode = newCh[++newStartIndex] } else if (newCh[newEndIndex] == undefined) { newEndVnode = newCh[--newEndIndex] } else if (sameVnode(oldStartVnode, newStartVnode)) { // ①、新前與舊前命中 console.log('①、新前與舊前命中'); //調用 patchVnode 對比兩個節點的 對象 文本 children patchVnode(oldStartVnode, newStartVnode) // 指針下移改變節點 oldStartVnode = oldCh[++oldStartIndex] newStartVnode = newCh[++newStartIndex] } else if (sameVnode(oldEndVnode, newEndVnode)) { // ②、新后與舊后命中 console.log('②、新后與舊后命中'); //調用 patchVnode 對比兩個節點的 對象 文本 children patchVnode(oldStartVnode, newStartVnode) // 指針下移并改變節點 oldEndVnode = oldCh[--oldEndIndex] newEndVnode = newCh[--newEndIndex] } else if (sameVnode(oldStartVnode, newEndVnode)) { // ③、新后與舊前命中 patchVnode(oldStartVnode, newEndVnode) console.log(newEndVnode); // 移動節點 當③新后與舊前命中的時候,此時要移動節點, // 移動 新后(舊前兩者指向的是同一節點) 指向的節點到舊節點的 舊后的后面,并且找到舊節點中對應的節點,然后在舊的虛擬節點中將這個節點設置為undefined。 parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling) // 在上面動畫中 命中③是在舊節點的后面插入的 所以使用nextSibling // 指針下移并改變節點 oldStartVnode = oldCh[++oldStartIndex] newEndVnode = newCh[--newEndIndex] } else if (sameVnode(oldEndVnode, newStartVnode)) { // ④、新前與舊后命中 patchVnode(oldEndVnode, newStartVnode) // 移動節點 // 當`④新前與舊后`命中的時候,此時要移動節點,移動`新前(舊后指向同一個節點)`指向的這個節點到舊節點的 `舊前的前面`, //并且找到`舊節點中對應的節點`,然后在`舊的虛擬節點中將這個節點設置為undefined` parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm) //指針下移并改變節點 oldEndVnode = oldCh[--oldEndIndex] newStartVnode = newCh[++newStartIndex] } else { // 四種都沒有命中 console.log(11); //kepMap作為緩存不用每次遍歷對象 if (!keyMap) { keyMap = {} // 遍歷舊的節點 for (let i = oldStartIndex; i <= oldEndIndex; i++) { // 獲取舊節點的key const key = oldCh[i].data.key if (key != undefined) { //key不為空 并且將key存放到keyMap對象中 keyMap[key] = i } } } // 取出newCh中的的key 并找出在keyMap中的位置 并映射到oldCh中 const oldIndex = keyMap[newStartVnode.key] if (oldIndex == undefined) { // 新增 console.log(111); parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm) } else { // 移動位置 // 取出需要移動的項 const elmToMove = oldCh[oldIndex] // 判斷是選擇器是否一樣 patchVnode(elmToMove, newStartVnode) // 標記已經處理過了 oldCh[oldIndex] = undefined // 移動節點 移動到舊前前面 因為舊前與舊后之間要被刪除 parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm) } // 只移動新的節點 newStartVnode = newCh[++newStartIndex] } } //循環結束 還有剩余節點沒處理 if (newStartIndex <= newEndIndex) { //說明 新節點還有未處理的節點,意味著需要添加節點 console.log('新增節點'); // 創建標桿 console.log(newCh[newEndIndex + 1]); // 節點插入的標桿位置 官方源碼的寫法 但是我們寫代碼新的虛擬節點中,elm設置了undefined 所以這里永遠都是會在后面插入 小bug // let before = newCh[newEndIndex + 1] == null ? null : newCh[newEndIndex + 1].elm // 若不想出現bug 可以在插入節點中直接oldCh[oldStartIndex].elm 但是也會出現不一樣的bug 所以重在學習思路 for (let i = newStartIndex; i <= newEndIndex; i++) { // 插入節點 因為舊節點遍歷完之后 新節點還有剩余節點 這里需要使用crateElement函數新建一個真實DOM節點 // insertBefore會自動識別null parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIndex].elm) } } else if (oldStartIndex <= oldEndIndex) { //說明舊節點還有剩余節點還沒有處理 意味著需要刪除節點 console.log('刪除節點'); for (let i = oldStartIndex; i <= oldEndIndex; i++) { if(oldCh[i]) parentElm.removeChild(oldCh[i].elm) } } }
關于“Vue中的虛擬DOM和Diff算法實例分析”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Vue中的虛擬DOM和Diff算法實例分析”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。