本篇文章给大家带来的内容是关于虚拟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-diff2import 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 }}// 子数的difffunction 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 }}// 子数的difffunction 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.
以上就是虚拟dom原理流程的分析与实现的详细内容。