大三戰五渣的我,平時也就只能用用別人的輪子,可總用不順心,畢竟不知道原理,最近用vue寫項目,裏面涉及到的Virtual DOM雖然已不是什麼新概念,但我也只是據說而已,不知其因此然,既然看到大佬們解析後,那就記錄下吧
參考資料:
戴嘉華:https://github.com/livoras/bl...
張歆琳:https://www.jianshu.com/p/616...
王沛:https://www.infoq.cn/article/...前端
首先先了解一下加載一個HTML會發生哪些事情vue
當你用傳統的源生api或jQuery去操做DOM時,瀏覽器會從構建DOM樹開始從頭至尾執行一遍流程。好比當你在一次操做時,須要更新10個DOM節點,理想狀態是一次性構建完DOM樹,再執行後續操做。但瀏覽器沒這麼智能,收到第一個更新DOM請求後,並不知道後續還有9次更新操做,所以會立刻執行流程,最終執行10次流程。顯然例如計算DOM節點的座標值等都是白白浪費性能,可能此次計算完,緊接着的下一個DOM更新請求,這個節點的座標值就變了,前面的一次計算是無用功。
DOM是很慢的,咱們能夠打印一下一個簡單的div元素的屬性node
這還只是一層而已,真實的DOM會更加龐大,輕微的觸碰可能就會致使頁面重排,這但是殺死性能的罪魁禍首。而相對於操做DOM對象,原生的JS對象處理起來更快並且簡單react
在 JS 和 DOM 之間作了一個緩存。能夠類比 CPU 和硬盤,既然硬盤這麼慢,咱們就在它們之間加個緩存:既然 DOM 這麼慢,咱們就在它們 JS 和 DOM 之間加個緩存。CPU(JS)只操做內存(Virtual DOM),最後的時候再把變動寫入硬盤(DOM)。git
用JS記錄節點的類型,屬性和子節點
element.jsgithub
function Element (tagName, props, children) { this.tagName = tagName this.props = props this.children = children } function el(tagName, props, children){ return new Element(tagName, props, children) }
例如上面的 DOM 結構就能夠簡單的表示:算法
let el = require('./element') let div= el('div', {id: 'blue-div'}, [ el('p', {class: 'pink-p'}, [ el('span', {class: 'yellow-sapn'}, ['Virtual sapn'])]), el('ul', {class: 'green-ul'}, [ el('li', {class: 'red-li'}, ['Virtual li1']), el('li', {class: 'red-li'}, ['Virtual li2']), el('li', {class: 'red-li'}, ['Virtual li3'])]), el('div', {class: 'black-div'}, ['Virtual div']) ])
如今的div
只是一個JS對象表示的DOM結構,頁面上並無這個結構,下面用來構建真正的div
api
Element.prototype.render = function () { let el = document.createElement(this.tagName) //根據tagName構建 let props = this.props for (let propName in props) { // 設置節點的DOM屬性 let propValue = props[propName] el.setAttribute(propName, propValue) } let children = this.children || [] children.forEach(function (child) { let childEl = (child instanceof Element) ? child.render() // 若是子節點也是虛擬DOM,遞歸構建DOM節點 : document.createTextNode(child) // 若是字符串,只構建文本節點 el.appendChild(childEl) }) return el }
render方法會根據tagName構建一個真正的DOM節點,而後設置這個節點的屬性,最後遞歸地把本身的子節點也構建起來。因此只須要:數組
let divRoot = div.render() document.body.appendChild(divRoot)
上面的運行結果:瀏覽器
兩棵樹的徹底差別比較的時間複雜度爲O(n^3),這是很差的,又由於前端不會常常進行跨層地移動DOM元素,因此Virtual DOM只對同一層級的元素進行比較,從而時間複雜度降爲O(n)
在實際的代碼中,會對新舊兩棵樹進行一個深度優先的遍歷,這樣每一個節點都會有一個惟一的標記,在深度優先遍歷的時候,每遍歷到一個節點就把改節點和新的數進行對比,若是有差別就記錄到patches
中
// diff 函數,對比兩棵樹 function diff (oldTree, newTree) { let index = 0 // 當前節點的標誌 let patches = {} // 用來記錄每一個節點差別的對象 dfsWalk(oldTree, newTree, index, patches) return patches } // 對兩棵樹進行深度優先遍歷 function dfsWalk (oldNode, newNode, index, patches) { // 對比oldNode和newNode的不一樣,記錄下來 patches[index] = [...] diffChildren(oldNode.children, newNode.children, index, patches) } // 遍歷子節點 function diffChildren (oldChildren, newChildren, index, patches) { let leftNode = null let currentNodeIndex = index oldChildren.forEach(function (child, i) { let newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) // 計算節點的標識 ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍歷子節點 leftNode = child }) }
例如,上面的div和新的div有差別,當前的標記是0,那麼:
patches[0] = [{difference}, {difference}, ...] // 用數組存儲新舊節點的不一樣
上面出現了四種新舊樹不一樣的狀況:
p
變成了div
,將舊節點卸載並裝載新節點li
)、刪除節點,實際操做如圖:因此咱們定義了幾種差別類型:
let REPLACE = 0 patches[0] = [{ type: REPALCE, node: newNode // el('div', props, children) p換成div }] let PROPS = 1 patches[0] = [{ type: REPALCE, node: newNode // el('p', props, children) }, { type: PROPS, props: {//給p新增了id爲container id: "container" } }] let TEXT = 2 patches[1] = [{//修改文本節點 type: TEXT, content: "Virtual DOM2" }] let REORDER = 3 //重排見王沛的https://www.infoq.cn/article/react-dom-diff
最終Diff出來的結果類型以下:
{ 1: [ {type: REPLACE, node: Element} ], 4: [ {type: TEXT, content: "after update"} ], 5: [ {type: PROPS, props: {class: "marginLeft10"}}, {type: REORDER, moves: [{index: 2, type: 0}]} ], 6: [ {type: REORDER, moves: [{index: 2, type: 0}]} ], 8: [ {type: REORDER, moves: [{index: 2, type: 0}]} ], 9: [ {type: TEXT, content: "Item 3"} ], }
由於步驟一所構建的 JavaScript 對象樹和render出來真正的DOM樹的信息、結構是同樣的。因此咱們能夠對那棵DOM樹也進行深度優先的遍歷,遍歷的時候從步驟二生成的patches對象中找出當前遍歷的節點差別,而後進行 DOM 操做。
function patch (node, patches) { let walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { let currentPatches = patches[walker.index] // 從patches拿出當前節點的差別 let len = node.childNodes ? node.childNodes.length : 0 for (let i = 0; i < len; i++) { // 深度遍歷子節點 let child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } if (currentPatches) { applyPatches(node, currentPatches) // 對當前節點進行DOM操做 } }
applyPatches,根據不一樣類型的差別對當前節點進行 DOM 操做:
function applyPatches (node, currentPatches) { currentPatches.forEach(function (currentPatch) { switch (currentPatch.type) { case REPLACE: node.parentNode.replaceChild(currentPatch.node.render(), node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
Virtual DOM 算法主要是實現上面步驟的三個函數:element,diff,patch。而後就能夠實際的進行使用:
// 1. 構建虛擬DOM let tree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: blue'}, ['simple virtal dom']), el('p', ['Hello, virtual-dom']), el('ul', [el('li')]) ]) // 2. 經過虛擬DOM構建真正的DOM let root = tree.render() document.body.appendChild(root) // 3. 生成新的虛擬DOM let newTree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: red'}, ['simple virtal dom']), el('p', ['Hello, virtual-dom']), el('ul', [el('li'), el('li')]) ]) // 4. 比較兩棵虛擬DOM樹的不一樣 let patches = diff(tree, newTree) // 5. 在真正的DOM元素上應用變動 patch(root, patches)
原理加1,頭髮減一堆