虚拟dom原理流程的分析与实现
本篇文章给大家带来的内容是关于虚拟dom原理流程的分析与实现,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。
背景
大家都知道,在网页中浏览器资源开销最大便是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('p', { '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树,然后渲染到页面上
<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代码流程:
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.
Atas ialah kandungan terperinci 虚拟dom原理流程的分析与实现. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Alat AI Hot

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool
Gambar buka pakaian secara percuma

Clothoff.io
Penyingkiran pakaian AI

Video Face Swap
Tukar muka dalam mana-mana video dengan mudah menggunakan alat tukar muka AI percuma kami!

Artikel Panas

Alat panas

Notepad++7.3.1
Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1
Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6
Alat pembangunan web visual

SublimeText3 versi Mac
Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Topik panas





Cara menggunakan WebSocket dan JavaScript untuk melaksanakan sistem pengecaman pertuturan dalam talian Pengenalan: Dengan perkembangan teknologi yang berterusan, teknologi pengecaman pertuturan telah menjadi bahagian penting dalam bidang kecerdasan buatan. Sistem pengecaman pertuturan dalam talian berdasarkan WebSocket dan JavaScript mempunyai ciri kependaman rendah, masa nyata dan platform merentas, dan telah menjadi penyelesaian yang digunakan secara meluas. Artikel ini akan memperkenalkan cara menggunakan WebSocket dan JavaScript untuk melaksanakan sistem pengecaman pertuturan dalam talian.

WebSocket dan JavaScript: Teknologi utama untuk merealisasikan sistem pemantauan masa nyata Pengenalan: Dengan perkembangan pesat teknologi Internet, sistem pemantauan masa nyata telah digunakan secara meluas dalam pelbagai bidang. Salah satu teknologi utama untuk mencapai pemantauan masa nyata ialah gabungan WebSocket dan JavaScript. Artikel ini akan memperkenalkan aplikasi WebSocket dan JavaScript dalam sistem pemantauan masa nyata, memberikan contoh kod dan menerangkan prinsip pelaksanaannya secara terperinci. 1. Teknologi WebSocket

Pengenalan kepada cara menggunakan JavaScript dan WebSocket untuk melaksanakan sistem pesanan dalam talian masa nyata: Dengan populariti Internet dan kemajuan teknologi, semakin banyak restoran telah mula menyediakan perkhidmatan pesanan dalam talian. Untuk melaksanakan sistem pesanan dalam talian masa nyata, kami boleh menggunakan teknologi JavaScript dan WebSocket. WebSocket ialah protokol komunikasi dupleks penuh berdasarkan protokol TCP, yang boleh merealisasikan komunikasi dua hala masa nyata antara pelanggan dan pelayan. Dalam sistem pesanan dalam talian masa nyata, apabila pengguna memilih hidangan dan membuat pesanan

Cara menggunakan WebSocket dan JavaScript untuk melaksanakan sistem tempahan dalam talian Dalam era digital hari ini, semakin banyak perniagaan dan perkhidmatan perlu menyediakan fungsi tempahan dalam talian. Adalah penting untuk melaksanakan sistem tempahan dalam talian yang cekap dan masa nyata. Artikel ini akan memperkenalkan cara menggunakan WebSocket dan JavaScript untuk melaksanakan sistem tempahan dalam talian dan memberikan contoh kod khusus. 1. Apakah itu WebSocket? WebSocket ialah kaedah dupleks penuh pada sambungan TCP tunggal.

JavaScript dan WebSocket: Membina sistem ramalan cuaca masa nyata yang cekap Pengenalan: Hari ini, ketepatan ramalan cuaca sangat penting kepada kehidupan harian dan membuat keputusan. Apabila teknologi berkembang, kami boleh menyediakan ramalan cuaca yang lebih tepat dan boleh dipercayai dengan mendapatkan data cuaca dalam masa nyata. Dalam artikel ini, kita akan mempelajari cara menggunakan teknologi JavaScript dan WebSocket untuk membina sistem ramalan cuaca masa nyata yang cekap. Artikel ini akan menunjukkan proses pelaksanaan melalui contoh kod tertentu. Kami

Tutorial JavaScript: Bagaimana untuk mendapatkan kod status HTTP, contoh kod khusus diperlukan: Dalam pembangunan web, interaksi data dengan pelayan sering terlibat. Apabila berkomunikasi dengan pelayan, kami selalunya perlu mendapatkan kod status HTTP yang dikembalikan untuk menentukan sama ada operasi itu berjaya dan melaksanakan pemprosesan yang sepadan berdasarkan kod status yang berbeza. Artikel ini akan mengajar anda cara menggunakan JavaScript untuk mendapatkan kod status HTTP dan menyediakan beberapa contoh kod praktikal. Menggunakan XMLHttpRequest

Penggunaan: Dalam JavaScript, kaedah insertBefore() digunakan untuk memasukkan nod baharu dalam pepohon DOM. Kaedah ini memerlukan dua parameter: nod baharu untuk dimasukkan dan nod rujukan (iaitu nod di mana nod baharu akan dimasukkan).

JavaScript ialah bahasa pengaturcaraan yang digunakan secara meluas dalam pembangunan web, manakala WebSocket ialah protokol rangkaian yang digunakan untuk komunikasi masa nyata. Menggabungkan fungsi berkuasa kedua-duanya, kami boleh mencipta sistem pemprosesan imej masa nyata yang cekap. Artikel ini akan memperkenalkan cara untuk melaksanakan sistem ini menggunakan JavaScript dan WebSocket, dan memberikan contoh kod khusus. Pertama, kita perlu menjelaskan keperluan dan matlamat sistem pemprosesan imej masa nyata. Katakan kita mempunyai peranti kamera yang boleh mengumpul data imej masa nyata
