1.什么是虚拟dom
虚拟dom本质上是js对象,是对真实dom的抽象,在状态变化时,记录新树和旧树的差异,最后把差异更新到真实dom中。
首先Virtual DOM是一个映射真实DOM的JavaScript对象,如果需要改变任何元素的状态,那么是先在Virtual DOM上进行改变,而不是直接改变真实的DOM。当有变化产生时,一个新的Virtual DOM对象会被创建并计算新旧Virtual DOM之间的差别。之后这些差别会应用在真实的DOM上
<div class='box'>
<p class='item'>血糖</p>
<strong>标题</strong>
</div>
以上真实dom对应的虚拟dom如下:
let vNode = {
tagName:'div',
props:{
'class':'box'
},
children:[{
tagName:'p',
props:{
'class':'item'
},
text:'血糖'
},{
tagName:'strong',
props:{
'class':'item'
},
text:'标题'
}]
}
假如我们改变了部分文本内容:
<div class='box'>
<p class='item'>血糖</p>
<strong>修改的内容部分</strong>
</div>
新的虚拟dom就是:
let vNode = {
tagName:'div',
props:{
'class':'box'
},
children:[{
tagName:'p',
props:{
'class':'item'
},
text:'血糖'
},{
tagName:'strong',
props:{
'class':'item'
},
text:'修改的内容部分'
}]
}
比较新旧2个虚拟dom,我们用一个算法叫diff算法,他主要是找出差异,最小化的更新视图,所以diff算法本质上就是比较2个js对象的差异。 diff算法内部流程:
当数据改变的时候,就会触发我内部的setter方法,,进一步触发dep.notify方法,通知到各个数据使用方,执行patch方法。patch方法接收2个参数新旧虚拟节点。首先我在内部需要判断一下是不是同类标签,如果不是同类标签,那就没有比较的必要直接替换即可,如果是同一个标签,那就需要进一步执行patchVnode方法,在这个方法内部,也是首先需要判断一下新旧虚拟节点是否相等,如果相等就不用比较,直接return。如果不相等,那就需要分情况来对比,比对的原则就是以新虚拟节点的结果为准。第一种情况:新旧虚拟节点都有文本节点,直接用新的文本替换新的文本就行了。第二种情况:旧的虚拟dom没有子节点,新的虚拟dom有子节点,直接添加新的子节点。第三种:旧的有子节点,新的没有子节点,直接删除旧的子节点就行了。第四种:新旧都有子节点,我们就需要对比他们的子节点,所以我们内部封装了upadateChildren方法(首尾指针法),专门比对他们的子节点。
patchVnode
patchVnode(vNode,oldVnode)
snabbdom
要想了解虚拟dom,先了解以下snabbdom。snabbdom(github.com/snabbdom/sn…)
在这里看到官方给的一个example
这里可以看到列出来的两个主要的核心函数,即h()函数和patch()函数,我们先来看下h()函数:
h函数
可以看到创建的虚拟DOM树里面的结构在左边的vnode里都有体现,所以现在看来我们的虚拟DOM结构树和snabbdom中的h()函数是完全可以对应起来的,可以通过一个方法将虚拟DOM结构转化成vnode;而上图中newVnode则指的是虚拟DOM树中的数据发生变化之后生成的vnode
我们在回过头来看patch()函数
patch函数
patch函数的执行分为两个阶段,两次传递的参数都是两个
第一阶段为虚拟dom的第一次渲染,传递的两个参数分别是放真实DOM的container和生成的vnode,此时patch函数的作用是用来将初次生成的真实DOM结构挂载到指定的container上面。
第二阶段传递的两个参数分别为vnode和newVnode,此时patch函数的作用是使用diff算法对比两个参数的差异,进而更新参数变化的DOM节点
可以发现h函数和patch函数在snabbdom中实现vdom到真实DOM的转化起到了至关重要的作用,那么还有一个很重要的环节,patch函数中是怎么样实现对比两个vnode从而实现对真实DOM的更新的呢,这里还要提一下snabbdom的另外一个核心算法,即diff算法。
diff算法
但是此处是如何实现对vnode的对比的呢?参考以下代码
function updateChildren(vnode, newVnode) { // 创建对比函数
var children = vnode.children || []
var newChildren = newVnode.children || []
children.forEach(function(childrenVnode, index) {
var newChildVnode = newChildren[index] // 首先拿到对应新的节点
if (childrenVnode.tag === newChildVnode.tag) { // 判断节点是否相同
updateChilren(childrenVnode, newChildVnode) // 如果相同执行递归,深度对比节点
} else {
repleaseNode(childrenVnode, newChildVnode) // 如果不同则将旧的节点替换成新的节点
}
})
}
function repleaseNode(vnode, newVnode) { // 节点替换函数
var elem = vnode.elem
var newEle = createElement(newVnode)
}
upadateChildren方法
有以上2个虚拟dom,进行同级比较,采用首尾指针法。
不管新旧虚拟dom,都有首尾2个元素。首先旧虚拟节点的start和新虚拟节点的的start比较,如果没有比对成功,旧虚拟节点的start和新虚拟dom的end比对,如果还没有成功,旧虚拟节点的start和新虚拟节点的start比对,如果还是没比对成功,旧虚拟节点的end和新虚拟节点的end比对。
2.虚拟dom的好处
虚拟dom将dom的对比放在了js层,通过对比不同之处来选择新的渲染节点,从而提高渲染效率。
用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
借助snabbdom,vnode(老的虚拟节点),newVnode(新的虚拟节点),比较新旧借点是新增,删除,还是修改 好处:比较虚拟节点的变化,计算出一个最小的需要更新的视图,然后再去操作dom,没有变化的dom是不会重新渲染的
3.虚拟dom为什么能提高性能?
在使用的过程中,我们需要先将实体Dom转换成虚拟Dom,在处理完成之后,再将处理完成的虚拟Dom转化为实体Dom,这个过程中所消耗的性能也同样很多。但是为什么大家都说虚拟Dom比实体Dom快呢(或者说是虚拟Dom能够提高性能)?其实根本原因是就是使用了虚拟Dom能够减少实际Dom的操作次数,减少回流和重绘的次数
换句话说就是虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能
使用diff算法比较新旧虚拟DOM—-即比较两个js对象不怎么耗性能,而比较两个真实的DOM比较耗性能,从而虚拟DOM极大的提升了性能。
所以说为什么会使用虚拟Dom
减少不必要的重排和重绘(提高性能)
提高项目的可维护性(会降低部分性能,但是对于可维护性的角度来说,这部分性能的损耗是能够接受的)
4.diff算法(最小量更新)
以最小的代价对dom进行更新,本质就是比较两个js对象的差异。