摘要:虛擬原理流程簡單概括有三點(diǎn)用模擬樹,并渲染這個(gè)樹比較新老樹,得到比較的差異對象把差異對象應(yīng)用到渲染的樹。下面是流程圖下面我們用代碼一步步去實(shí)現(xiàn)一個(gè)流程圖用模擬樹并渲染到頁面上其實(shí)虛擬,就是用對象結(jié)構(gòu)的一種映射,下面我們一步步實(shí)現(xiàn)這個(gè)過程。
背景
大家都知道,在網(wǎng)頁中瀏覽器資源開銷最大便是DOM節(jié)點(diǎn)了,DOM很慢并且非常龐大,網(wǎng)頁性能問題大多數(shù)都是有JavaScript修改DOM所引起的。我們使用Javascript來操縱DOM,操作效率往往很低,由于DOM被表示為樹結(jié)構(gòu),每次DOM中的某些內(nèi)容都會(huì)發(fā)生變化,因此對DOM的更改非常快,但更改后的元素,并且它的子項(xiàng)必須經(jīng)過Reflow / Layout階段,然后瀏覽器必須重新繪制更改,這很慢的。因此,回流/重繪的次數(shù)越多,您的應(yīng)用程序就越卡頓。但是,Javascript運(yùn)行速度很快,虛擬DOM是放在JS 和 HTML中間的一個(gè)層。它可以通過新舊DOM的對比,來獲取對比之后的差異對象,然后有針對性的把差異部分真正地渲染到頁面上,從而減少實(shí)際DOM操作,最終達(dá)到性能優(yōu)化的目的。
虛擬dom原理流程簡單概括有三點(diǎn):
用JavaScript模擬DOM樹,并渲染這個(gè)DOM樹
比較新老DOM樹,得到比較的差異對象
把差異對象應(yīng)用到渲染的DOM樹。
下面是流程圖:
下面我們用代碼一步步去實(shí)現(xiàn)一個(gè)流程圖
用JavaScript模擬DOM樹并渲染到頁面上其實(shí)虛擬DOM,就是用JS對象結(jié)構(gòu)的一種映射,下面我們一步步實(shí)現(xiàn)這個(gè)過程。
我們用JS很容易模擬一個(gè)DOM樹的結(jié)構(gòu),例如用這樣的一個(gè)函數(shù)createEl(tagName, props, children)來創(chuàng)建DOM結(jié)構(gòu)。
tagName標(biāo)簽名、props是屬性的對象、children是子節(jié)點(diǎn)。
然后渲染到頁面上,代碼如下:
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)
通過上面的函數(shù),調(diào)用vdom.render()這樣子我們就很好的構(gòu)建了如下所示的一個(gè)DOM樹,然后渲染到頁面上
I am H1
- #list1
- #list2
I am p
下面我們看看CreactEl.js代碼流程:
import { setAttr } from "./utils" class CreateEl { constructor (tagName, props, children) { // 當(dāng)只有兩個(gè)參數(shù)的時(shí)候 例如 celement(el, [123]) if (Array.isArray(props)) { children = props props = {} } // tagName, props, children數(shù)據(jù)保存到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++ }) // 給每一個(gè)節(jié)點(diǎn)設(shè)置一個(gè)count this.count = count } // 構(gòu)建一個(gè) dom 樹 render () { // 創(chuàng)建dom const el = document.createElement(this.tagName) const props = this.props // 循環(huán)所有屬性,然后設(shè)置屬性 for (let [key, val] of Object.entries(props)) { setAttr(el, key, val) } this.children.forEach(child => { // 遞歸循環(huán) 構(gòu)建tree let childEl = (child instanceof CreateEl) ? child.render() : document.createTextNode(child) el.appendChild(childEl) }) return el } }
上面render函數(shù)的功能是把節(jié)點(diǎn)創(chuàng)建好,然后設(shè)置節(jié)點(diǎn)屬性,最后遞歸創(chuàng)建。這樣子我們就得到一個(gè)DOM樹,然后插入(appendChild)到頁面上。
比較新老dom樹,得到比較的差異對象上面,我們已經(jīng)創(chuàng)建了一個(gè)DOM樹,然后在創(chuàng)建一個(gè)不同的DOM樹,然后做比較,得到比較的差異對象。
比較兩棵DOM樹的差異,是虛擬DOM的最核心部分,這也是人們常說的虛擬DOM的diff算法,兩顆完全的樹差異比較一個(gè)時(shí)間復(fù)雜度為 O(n^3)。但是在我們的web中很少用到跨層級(jí)DOM樹的比較,所以一個(gè)層級(jí)跟一個(gè)層級(jí)對比,這樣算法復(fù)雜度就可以達(dá)到 O(n)。如下圖
其實(shí)在代碼中,我們會(huì)從根節(jié)點(diǎn)開始標(biāo)志遍歷,遍歷的時(shí)候把每個(gè)節(jié)點(diǎn)的差異(包括文本不同,屬性不同,節(jié)點(diǎn)不同)記錄保存起來。如下圖:
兩個(gè)節(jié)點(diǎn)之間的差異有總結(jié)起來有下面4種
0 直接替換原有節(jié)點(diǎn) 1 調(diào)整子節(jié)點(diǎn),包括移動(dòng)、刪除等 2 修改節(jié)點(diǎn)屬性 3 修改節(jié)點(diǎn)文本內(nèi)容
如下面兩棵樹比較,把差異記錄下來。
主要是簡歷一個(gè)遍歷index(看圖3),然后從根節(jié)點(diǎn)開始比較,比較萬之后記錄差異對象,繼續(xù)從左子樹比較,記錄差異,一直遍歷下去。主要流程如下
// 這是比較兩個(gè)樹找到最小移動(dòng)量的算法是Levenshtein距離,即O(n * m) // 具體請看 https://www.npmjs.com/package/list-diff2 import listDiff from "list-diff2" // 比較兩棵樹 function diff (oldTree, newTree) { // 節(jié)點(diǎn)的遍歷順序 let index = 0 // 在遍歷過程中記錄節(jié)點(diǎn)的差異 let patches = {} // 深度優(yōu)先遍歷兩棵樹 deepTraversal(oldTree, newTree, index, patches) // 得到的差異對象返回出去 return patches } function deepTraversal(oldNode, newNode, index, patches) { let currentPatch = [] // ...中間有很多對patches的處理 // 遞歸比較子節(jié)點(diǎn)是否相同 diffChildren(oldNode.children, newNode.children, index, patches, currentPatch) if (currentPatch.length) { // 那個(gè)index節(jié)點(diǎn)的差異記錄下來 patches[index] = currentPatch } } // 子數(shù)的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 }) }
然后我們調(diào)用完diff(tree, newTree)等到最后的差異對象是這樣子的。
{ "1": [ { "type": 0, "node": { "tagName": "h3", "props": { "style": "color: green" }, "children": [ "I am H1" ], "count": 1 } } ] ... }
key是代表那個(gè)節(jié)點(diǎn),這里我們是第二個(gè),也就是h1會(huì)改變成h3,還有省略的兩個(gè)差異對象代碼沒有貼出來~~
然后看下diff.js的完整代碼,如下
import listDiff from "list-diff2" // 每個(gè)節(jié)點(diǎn)有四種變動(dòng) export const REPLACE = 0 // 替換原有節(jié)點(diǎn) export const REORDER = 1 // 調(diào)整子節(jié)點(diǎn),包括移動(dòng)、刪除等 export const PROPS = 2 // 修改節(jié)點(diǎn)屬性 export const TEXT = 3 // 修改節(jié)點(diǎn)文本內(nèi)容 export function diff (oldTree, newTree) { // 節(jié)點(diǎn)的遍歷順序 let index = 0 // 在遍歷過程中記錄節(jié)點(diǎn)的差異 let patches = {} // 深度優(yōu)先遍歷兩棵樹 deepTraversal(oldTree, newTree, index, patches) // 得到的差異對象返回出去 return patches } function deepTraversal(oldNode, newNode, index, patches) { let currentPatch = [] if (newNode === null) { // 如果新節(jié)點(diǎn)沒有的話直接不用比較了 return } if (typeof oldNode === "string" && typeof newNode === "string") { // 比較文本節(jié)點(diǎn) if (oldNode !== newNode) { currentPatch.push({ type: TEXT, content: newNode }) } } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 節(jié)點(diǎn)類型相同 // 比較節(jié)點(diǎn)的屬性是否相同 let propasPatches = diffProps(oldNode, newNode) if (propasPatches) { currentPatch.push({ type: PROPS, props: propsPatches }) } // 遞歸比較子節(jié)點(diǎn)是否相同 diffChildren(oldNode.children, newNode.children, index, patches, currentPatch) } else { // 節(jié)點(diǎn)不一樣,直接替換 currentPatch.push({ type: REPLACE, node: newNode }) } if (currentPatch.length) { // 那個(gè)index節(jié)點(diǎn)的差異記錄下來 patches[index] = currentPatch } } // 子數(shù)的diff function diffChildren (oldChildren, newChildren, index, patches, currentPatch) { var diffs = listDiff(oldChildren, newChildren) newChildren = diffs.children // 如果調(diào)整子節(jié)點(diǎn),包括移動(dòng)、刪除等的話 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 // 聲明一個(gè)有沒沒有屬性變更的標(biāo)志 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 }
得到差異對象之后,剩下就是把差異對象應(yīng)用到我們的dom節(jié)點(diǎn)上面了。
把差異對象應(yīng)用到渲染的dom樹到了這里其實(shí)就簡單多了。我們上面得到的差異對象之后,然后選擇同樣的深度遍歷,如果那個(gè)節(jié)點(diǎn)有差異的話,判斷是上面4種中的哪一種,根據(jù)差異對象直接修改這個(gè)節(jié)點(diǎn)就可以了。
function patch (node, patches) { // 也是從0開始 const step = { index: 0 } // 深度遍歷 deepTraversal(node, step, patches) } // 深度優(yōu)先遍歷dom結(jié)構(gòu) function deepTraversal(node, step, patches) { // 拿到當(dāng)前差異對象 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) } //如果當(dāng)前節(jié)點(diǎn)存在差異 if (currentPatches) { // 把差異對象應(yīng)用到當(dāng)前節(jié)點(diǎn)上 applyPatches(node, currentPatches) } }
這樣子,調(diào)用patch(rootnode, patches)就直接有針對性的改變有差異的節(jié)點(diǎn)了。
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) } // 深度優(yōu)先遍歷dom結(jié)構(gòu) function deepTraversal(node, step, patches) { // 拿到當(dāng)前差異對象 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) } //如果當(dāng)前節(jié)點(diǎn)存在差異 if (currentPatches) { // 把差異對象應(yīng)用到當(dāng)前節(jié)點(diǎn)上 applyPatches(node, currentPatches) } } // 把差異對象應(yīng)用到當(dāng)前節(jié)點(diǎn)上 function applyPatches(node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { // 0: 替換原有節(jié)點(diǎn) case REPLACE: var newNode = (typeof currentPatch.node === "string") ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break // 1: 調(diào)整子節(jié)點(diǎn),包括移動(dòng)、刪除等 case REORDER: moveChildren(node, currentPatch.moves) break // 2: 修改節(jié)點(diǎn)屬性 case PROPS: for (let [key, val] of Object.entries(currentPatch.props)) { if (val === undefined) { node.removeAttribute(key) } else { setAttr(node, key, val) } } break; // 3:修改節(jié)點(diǎn)文本內(nèi)容 case TEXT: if (node.textContent) { node.textContent = currentPatch.content } else { node.nodeValue = currentPatch.content } break; default: throw new Error("Unknow patch type " + currentPatch.type); } }) } // 調(diào)整子節(jié)點(diǎn),包括移動(dòng)、刪除等 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) { // 變動(dòng)類型為刪除的節(jié)點(diǎn) 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原理已經(jīng)講完了,也簡單了實(shí)現(xiàn)了一個(gè)虛擬DOM,如果本文有什么不對的地方請指正。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/98275.html
摘要:根據(jù)標(biāo)準(zhǔn),到目前為止,一共有種緩存機(jī)制,有些是之前已有,有些是才新加入的。首次請求緩存有效期內(nèi)請求緩存過期后請求一般瀏覽器會(huì)將緩存記錄及緩存文件存在本地文件夾中。 騰訊 Bugly 特約作者:賀輝超 1. H5 緩存機(jī)制介紹 H5,即 HTML5,是新一代的 HTML 標(biāo)準(zhǔn),加入很多新的特性。離線存儲(chǔ)(也可稱為緩存機(jī)制)是其中一個(gè)非常重要的特性。H5 引入的離線存儲(chǔ),這意味著 web ...
摘要:緩存緩存,也叫網(wǎng)關(guān)緩存反向代理緩存。瀏覽器先向網(wǎng)關(guān)發(fā)起請求,網(wǎng)關(guān)服務(wù)器后面對應(yīng)著一臺(tái)或多臺(tái)負(fù)載均衡源服務(wù)器,會(huì)根據(jù)它們的負(fù)載請求,動(dòng)態(tài)將請求轉(zhuǎn)發(fā)到合適的源服務(wù)器上。雖然這種架構(gòu)負(fù)載均衡源服務(wù)器之間的緩存沒法共享,但卻擁有更好的處擴(kuò)展性。 一、前言? 工作上遇到一個(gè)這樣的需求,一個(gè)H5頁面在APP端,如果勾選已讀狀態(tài),則下次打開該鏈接,會(huì)跳過此頁面。用到了HTML5 的本地存儲(chǔ) API ...
摘要:模塊和將下面的渲染機(jī)制,安全機(jī)制,插件機(jī)制等等隱藏起來,提供一個(gè)接口層。進(jìn)行網(wǎng)頁的渲染進(jìn)程,可能有多個(gè)。最后進(jìn)程將結(jié)果由線程傳遞給進(jìn)程最后,進(jìn)程接收到結(jié)果并將結(jié)果繪制出來。 這是之前在簡書上面的處女作,也搬過來了,以后就一直在 segmentfault 上面寫文章了,webkit技術(shù)內(nèi)幕-朱永盛是我大四買的書,很舊的一本書了,當(dāng)時(shí)只看了一點(diǎn)點(diǎn),一直沒繼續(xù)看完它,現(xiàn)在才看完,,,說來慚愧...
摘要:歡迎來我的個(gè)人站點(diǎn)性能優(yōu)化其他優(yōu)化瀏覽器關(guān)鍵渲染路徑開啟性能優(yōu)化之旅高性能滾動(dòng)及頁面渲染優(yōu)化理論寫法對壓縮率的影響唯快不破應(yīng)用的個(gè)優(yōu)化步驟進(jìn)階鵝廠大神用直出實(shí)現(xiàn)網(wǎng)頁瞬開緩存網(wǎng)頁性能管理詳解寫給后端程序員的緩存原理介紹年底補(bǔ)課緩存機(jī)制優(yōu)化動(dòng) 歡迎來我的個(gè)人站點(diǎn) 性能優(yōu)化 其他 優(yōu)化瀏覽器關(guān)鍵渲染路徑 - 開啟性能優(yōu)化之旅 高性能滾動(dòng) scroll 及頁面渲染優(yōu)化 理論 | HTML寫法...
閱讀 2882·2021-09-22 15:58
閱讀 2392·2019-08-29 16:06
閱讀 1075·2019-08-29 14:14
閱讀 2959·2019-08-29 13:48
閱讀 2598·2019-08-28 18:01
閱讀 1709·2019-08-28 17:52
閱讀 3476·2019-08-26 14:05
閱讀 1814·2019-08-26 13:50