你們都知道,在網頁中瀏覽器資源開銷最大即是DOM節點了,DOM很慢而且很是龐大,網頁性能問題大多數都是有JavaScript修改DOM所引發的。咱們使用Javascript來操縱DOM,操做效率每每很低,因爲DOM被表示爲樹結構,每次DOM中的某些內容都會發生變化,所以對DOM的更改很是快,但更改後的元素,而且它的子項必須通過Reflow / Layout階段,而後瀏覽器必須從新繪製更改,這很慢的。所以,迴流/重繪的次數越多,您的應用程序就越卡頓。可是,Javascript運行速度很快,虛擬DOM是放在JS 和 HTML中間的一個層。它能夠經過新舊DOM的對比,來獲取對比以後的差別對象,而後有針對性的把差別部分真正地渲染到頁面上,從而減小實際DOM操做,最終達到性能優化的目的。node
簡單歸納有三點:react
下面是流程圖:web
下面咱們用代碼一步步去實現一個流程圖算法
其實虛擬DOM,就是用JS對象結構的一種映射,下面咱們一步步實現這個過程。npm
咱們用JS很容易模擬一個DOM樹的結構,例如用這樣的一個函數createEl(tagName, props, children)
來建立DOM結構。瀏覽器
tagName
標籤名、props
是屬性的對象、children
是子節點。
而後渲染到頁面上,代碼以下:性能優化
const createEl = (tagName, props, children) => new CreactEl(tagName, props, children) const vdom = createEl('div', { 'id': 'box' }, [ createEl('h1', { style: 'color: pink' }, ['I am H1']), createEl('ul', {class: 'list'}, [createEl('li', ['#list1']), createEl('li', ['#list2'])]), createEl('p', ['I am p']) ]) const rootnode = vdom.render() document.body.appendChild(rootnode)
經過上面的函數,調用vdom.render()
這樣子咱們就很好的構建了以下所示的一個DOM樹,而後渲染到頁面上app
<div id="box"> <h1 style="color: pink;">I am H1</h1> <ul class="list"> <li>#list1</li> <li>#list2</li> </ul> <p>I am p</p> </div>
下面咱們看看CreactEl.js代碼流程:dom
import { setAttr } from './utils' class CreateEl { constructor (tagName, props, children) { // 當只有兩個參數的時候 例如 celement(el, [123]) if (Array.isArray(props)) { children = props props = {} } // tagName, props, children數據保存到this對象上 this.tagName = tagName this.props = props || {} this.children = children || [] this.key = props ? props.key : undefined let count = 0 this.children.forEach(child => { if (child instanceof CreateEl) { count += child.count } else { child = '' + child } count++ }) // 給每個節點設置一個count this.count = count } // 構建一個 dom 樹 render () { // 建立dom const el = document.createElement(this.tagName) const props = this.props // 循環全部屬性,而後設置屬性 for (let [key, val] of Object.entries(props)) { setAttr(el, key, val) } this.children.forEach(child => { // 遞歸循環 構建tree let childEl = (child instanceof CreateEl) ? child.render() : document.createTextNode(child) el.appendChild(childEl) }) return el } }
上面render
函數的功能是把節點建立好,而後設置節點屬性,最後遞歸建立。這樣子咱們就獲得一個DOM樹,而後插入(appendChild)到頁面上。ide
上面,咱們已經建立了一個DOM樹,而後在建立一個不一樣的DOM樹,而後作比較,獲得比較的差別對象。
比較兩棵DOM樹的差別,是虛擬DOM的最核心部分,這也是人們常說的虛擬DOM的diff算法,兩顆徹底的樹差別比較一個時間複雜度爲 O(n^3)。可是在咱們的web中不多用到跨層級DOM樹的比較,因此一個層級跟一個層級對比,這樣算法複雜度就能夠達到 O(n)。以下圖
其實在代碼中,咱們會從根節點開始標誌遍歷,遍歷的時候把每一個節點的差別(包括文本不一樣,屬性不一樣,節點不一樣)記錄保存起來。以下圖:
兩個節點之間的差別有總結起來有下面4種
0 直接替換原有節點 1 調整子節點,包括移動、刪除等 2 修改節點屬性 3 修改節點文本內容
以下面兩棵樹比較,把差別記錄下來。
主要是簡歷一個遍歷index(看圖3),而後從根節點開始比較,比較萬以後記錄差別對象,繼續從左子樹比較,記錄差別,一直遍歷下去。主要流程以下
// 這是比較兩個樹找到最小移動量的算法是Levenshtein距離,即O(n * m) // 具體請看 https://www.npmjs.com/package/list-diff2 import listDiff from 'list-diff2' // 比較兩棵樹 function diff (oldTree, newTree) { // 節點的遍歷順序 let index = 0 // 在遍歷過程當中記錄節點的差別 let patches = {} // 深度優先遍歷兩棵樹 deepTraversal(oldTree, newTree, index, patches) // 獲得的差別對象返回出去 return patches } function deepTraversal(oldNode, newNode, index, patches) { let currentPatch = [] // ...中間有不少對patches的處理 // 遞歸比較子節點是否相同 diffChildren(oldNode.children, newNode.children, index, patches, currentPatch) if (currentPatch.length) { // 那個index節點的差別記錄下來 patches[index] = currentPatch } } // 子數的diff function diffChildren (oldChildren, newChildren, index, patches, currentPatch) { const diffs = listDiff(oldChildren, newChildren) newChildren = diffs.children // ...省略記錄差別對象 let leftNode = null let currentNodeIndex = index oldChildren.forEach((child, i) => { const newChild = newChildren[i] // index相加 currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 // 深度遍歷,遞歸 deepTraversal(child, newChild, currentNodeIndex, patches) // 從左樹開始 leftNode = child }) }
而後咱們調用完diff(tree, newTree)
等到最後的差別對象是這樣子的。
{ "1": [ { "type": 0, "node": { "tagName": "h3", "props": { "style": "color: green" }, "children": [ "I am H1" ], "count": 1 } } ] ... }
key
是表明那個節點,這裏咱們是第二個,也就是h1
會改變成h3
,還有省略的兩個差別對象代碼沒有貼出來~~
而後看下diff.js的完整代碼,以下
import listDiff from 'list-diff2' // 每一個節點有四種變更 export const REPLACE = 0 // 替換原有節點 export const REORDER = 1 // 調整子節點,包括移動、刪除等 export const PROPS = 2 // 修改節點屬性 export const TEXT = 3 // 修改節點文本內容 export function diff (oldTree, newTree) { // 節點的遍歷順序 let index = 0 // 在遍歷過程當中記錄節點的差別 let patches = {} // 深度優先遍歷兩棵樹 deepTraversal(oldTree, newTree, index, patches) // 獲得的差別對象返回出去 return patches } function deepTraversal(oldNode, newNode, index, patches) { let currentPatch = [] if (newNode === null) { // 若是新節點沒有的話直接不用比較了 return } if (typeof oldNode === 'string' && typeof newNode === 'string') { // 比較文本節點 if (oldNode !== newNode) { currentPatch.push({ type: TEXT, content: newNode }) } } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 節點類型相同 // 比較節點的屬性是否相同 let propasPatches = diffProps(oldNode, newNode) if (propasPatches) { currentPatch.push({ type: PROPS, props: propsPatches }) } // 遞歸比較子節點是否相同 diffChildren(oldNode.children, newNode.children, index, patches, currentPatch) } else { // 節點不同,直接替換 currentPatch.push({ type: REPLACE, node: newNode }) } if (currentPatch.length) { // 那個index節點的差別記錄下來 patches[index] = currentPatch } } // 子數的diff function diffChildren (oldChildren, newChildren, index, patches, currentPatch) { var diffs = listDiff(oldChildren, newChildren) newChildren = diffs.children // 若是調整子節點,包括移動、刪除等的話 if (diffs.moves.length) { var reorderPatch = { type: REORDER, moves: diffs.moves } currentPatch.push(reorderPatch) } var leftNode = null var currentNodeIndex = index oldChildren.forEach((child, i) => { var newChild = newChildren[i] // index相加 currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 // 深度遍歷,從左樹開始 deepTraversal(child, newChild, currentNodeIndex, patches) // 從左樹開始 leftNode = child }) } // 記錄屬性的差別 function diffProps (oldNode, newNode) { let count = 0 // 聲明一個有沒沒有屬性變動的標誌 const oldProps = oldNode.props const newProps = newNode.props const propsPatches = {} // 找出不一樣的屬性 for (let [key, val] of Object.entries(oldProps)) { // 新的不等於舊的 if (newProps[key] !== val) { count++ propsPatches[key] = newProps[key] } } // 找出新增的屬性 for (let [key, val] of Object.entries(newProps)) { if (!oldProps.hasOwnProperty(key)) { count++ propsPatches[key] = val } } // 沒有新增 也沒有不一樣的屬性 直接返回null if (count === 0) { return null } return propsPatches }
獲得差別對象以後,剩下就是把差別對象應用到咱們的dom節點上面了。
到了這裏其實就簡單多了。咱們上面獲得的差別對象以後,而後選擇一樣的深度遍歷,若是那個節點有差別的話,判斷是上面4種中的哪種,根據差別對象直接修改這個節點就能夠了。
function patch (node, patches) { // 也是從0開始 const step = { index: 0 } // 深度遍歷 deepTraversal(node, step, patches) } // 深度優先遍歷dom結構 function deepTraversal(node, step, patches) { // 拿到當前差別對象 const currentPatches = patches[step.index] const len = node.childNodes ? node.childNodes.length : 0 for (let i = 0; i < len; i++) { const child = node.childNodes[i] step.index++ deepTraversal(child, step, patches) } //若是當前節點存在差別 if (currentPatches) { // 把差別對象應用到當前節點上 applyPatches(node, currentPatches) } }
這樣子,調用patch(rootnode, patches)
就直接有針對性的改變有差別的節點了。
path.js完整代碼以下:
import {REPLACE, REORDER, PROPS, TEXT} from './diff' import { setAttr } from './utils' export function patch (node, patches) { // 也是從0開始 const step = { index: 0 } // 深度遍歷 deepTraversal(node, step, patches) } // 深度優先遍歷dom結構 function deepTraversal(node, step, patches) { // 拿到當前差別對象 const currentPatches = patches[step.index] const len = node.childNodes ? node.childNodes.length : 0 for (let i = 0; i < len; i++) { const child = node.childNodes[i] step.index++ deepTraversal(child, step, patches) } //若是當前節點存在差別 if (currentPatches) { // 把差別對象應用到當前節點上 applyPatches(node, currentPatches) } } // 把差別對象應用到當前節點上 function applyPatches(node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { // 0: 替換原有節點 case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break // 1: 調整子節點,包括移動、刪除等 case REORDER: moveChildren(node, currentPatch.moves) break // 2: 修改節點屬性 case PROPS: for (let [key, val] of Object.entries(currentPatch.props)) { if (val === undefined) { node.removeAttribute(key) } else { setAttr(node, key, val) } } break; // 3:修改節點文本內容 case TEXT: if (node.textContent) { node.textContent = currentPatch.content } else { node.nodeValue = currentPatch.content } break; default: throw new Error('Unknow patch type ' + currentPatch.type); } }) } // 調整子節點,包括移動、刪除等 function moveChildren (node, moves) { let staticNodelist = Array.from(node.childNodes) const maps = {} staticNodelist.forEach(node => { if (node.nodeType === 1) { const key = node.getAttribute('key') if (key) { maps[key] = node } } }) moves.forEach(move => { const index = move.index if (move.type === 0) { // 變更類型爲刪除的節點 if (staticNodeList[index] === node.childNodes[index]) { node.removeChild(node.childNodes[index]); } staticNodeList.splice(index, 1); } else { let insertNode = maps[move.item.key] ? maps[move.item.key] : (typeof move.item === 'object') ? move.item.render() : document.createTextNode(move.item) staticNodelist.splice(index, 0, insertNode); node.insertBefore(insertNode, node.childNodes[index] || null) } }) }
到這裏,最基本的虛擬DOM原理已經講完了,也簡單了實現了一個虛擬DOM,若是本文有什麼不對的地方請指正。