摘要:虛擬原理流程簡單概括有三點用模擬樹,并渲染這個樹比較新老樹,得到比較的差異對象把差異對象應用到渲染的樹。下面是流程圖下面我們用代碼一步步去實現一個流程圖用模擬樹并渲染到頁面上其實虛擬,就是用對象結構的一種映射,下面我們一步步實現這個過程。
背景
大家都知道,在網頁中瀏覽器資源開銷最大便是DOM節點了,DOM很慢并且非常龐大,網頁性能問題大多數都是有JavaScript修改DOM所引起的。我們使用Javascript來操縱DOM,操作效率往往很低,由于DOM被表示為樹結構,每次DOM中的某些內容都會發生變化,因此對DOM的更改非常快,但更改后的元素,并且它的子項必須經過Reflow / Layout階段,然后瀏覽器必須重新繪制更改,這很慢的。因此,回流/重繪的次數越多,您的應用程序就越卡頓。但是,Javascript運行速度很快,虛擬DOM是放在JS 和 HTML中間的一個層。它可以通過新舊DOM的對比,來獲取對比之后的差異對象,然后有針對性的把差異部分真正地渲染到頁面上,從而減少實際DOM操作,最終達到性能優化的目的。
虛擬dom原理流程簡單概括有三點:
用JavaScript模擬DOM樹,并渲染這個DOM樹
比較新老DOM樹,得到比較的差異對象
把差異對象應用到渲染的DOM樹。
下面是流程圖:
下面我們用代碼一步步去實現一個流程圖
用JavaScript模擬DOM樹并渲染到頁面上其實虛擬DOM,就是用JS對象結構的一種映射,下面我們一步步實現這個過程。
我們用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樹,然后渲染到頁面上
I am H1
- #list1
- #list2
I am p
下面我們看看CreactEl.js代碼流程:
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)到頁面上。
比較新老dom樹,得到比較的差異對象上面,我們已經創建了一個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節點上面了。
把差異對象應用到渲染的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,如果本文有什么不對的地方請指正。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98275.html
摘要:根據標準,到目前為止,一共有種緩存機制,有些是之前已有,有些是才新加入的。首次請求緩存有效期內請求緩存過期后請求一般瀏覽器會將緩存記錄及緩存文件存在本地文件夾中。 騰訊 Bugly 特約作者:賀輝超 1. H5 緩存機制介紹 H5,即 HTML5,是新一代的 HTML 標準,加入很多新的特性。離線存儲(也可稱為緩存機制)是其中一個非常重要的特性。H5 引入的離線存儲,這意味著 web ...
摘要:緩存緩存,也叫網關緩存反向代理緩存。瀏覽器先向網關發起請求,網關服務器后面對應著一臺或多臺負載均衡源服務器,會根據它們的負載請求,動態將請求轉發到合適的源服務器上。雖然這種架構負載均衡源服務器之間的緩存沒法共享,但卻擁有更好的處擴展性。 一、前言? 工作上遇到一個這樣的需求,一個H5頁面在APP端,如果勾選已讀狀態,則下次打開該鏈接,會跳過此頁面。用到了HTML5 的本地存儲 API ...
摘要:模塊和將下面的渲染機制,安全機制,插件機制等等隱藏起來,提供一個接口層。進行網頁的渲染進程,可能有多個。最后進程將結果由線程傳遞給進程最后,進程接收到結果并將結果繪制出來。 這是之前在簡書上面的處女作,也搬過來了,以后就一直在 segmentfault 上面寫文章了,webkit技術內幕-朱永盛是我大四買的書,很舊的一本書了,當時只看了一點點,一直沒繼續看完它,現在才看完,,,說來慚愧...
閱讀 2686·2021-09-22 15:58
閱讀 2230·2019-08-29 16:06
閱讀 898·2019-08-29 14:14
閱讀 2810·2019-08-29 13:48
閱讀 2451·2019-08-28 18:01
閱讀 1495·2019-08-28 17:52
閱讀 3318·2019-08-26 14:05
閱讀 1610·2019-08-26 13:50