⼀篇⽂章带你搞懂Vue虚拟Dom与diff算法
前⾔
使⽤过Vue和React的⼩伙伴肯定对虚拟Dom和diff算法很熟悉,它扮演着很重要的⾓⾊。由于⼩编接触Vue⽐较多,React只是浅学,所以本篇主要针对Vue来展开介绍,带你⼀步⼀步搞懂它。
虚拟DOM
vue与react面试题什么是虚拟DOM?
虚拟DOM(Virtual  Dom),也就是我们常说的虚拟节点,是⽤JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,⽤于对⽐虚拟DOM和真实DOM的差异,从⽽进⾏局部渲染来达到优化性能的⽬的。
真实的元素节点:
<div id="wrap">
<p class="title">Hello world!</p>
</div>
VNode:
{
tag:'div',
attrs:{
id:'wrap'
},
children:[
{
tag:'p',
text:'Hello world!',
attrs:{
class:'title',
}
}
]
}
为什么使⽤虚拟DOM?
简单了解虚拟DOM后,是不是有⼩伙伴会问:Vue和React框架中为什么会⽤到它呢?好问题!那来解决下⼩伙伴的疑问。起初我们在使⽤JS/JQuery时,不可避免的会⼤量操作DOM,⽽DOM的变化⼜会引发回流或重绘,从⽽降低页⾯渲染性能。那么怎样来减少对DOM的操作呢?此时虚拟DOM应⽤⽽⽣,所以虚拟DOM出现的主要⽬的就是为了减少频繁操作DOM⽽引起回流重绘所引发的性能问题的!
虚拟DOM的作⽤是什么?
1. 兼容性好。因为Vnode本质是JS对象,所以不管Node还是浏览器环境,都可以操作;
2. 减少了对Dom的操作。页⾯中的数据和状态变化,都通过Vnode对⽐,只需要在⽐对完之后更新DOM,不需要频繁操
作,提⾼了页⾯性能;
虚拟DOM和真实DOM的区别?
说到这⾥,那么虚拟DOM和真实DOM的区别是什么呢?总结⼤概如下:
虚拟DOM不会进⾏回流和重绘;
真实DOM在频繁操作时引发的回流重绘导致性能很低;
虚拟DOM频繁修改,然后⼀次性对⽐差异并修改真实DOM,最后进⾏依次回流重绘,减少了真实DOM中多次回流重绘引起的性能损耗;
虚拟DOM有效降低⼤⾯积的重绘与排版,因为是和真实DOM对⽐,更新差异部分,所以只渲染局部;
总损耗 = 真实DOM增删改 + (多节点)回流/重绘;    //计算使⽤真实DOM的损耗
总损耗 = 虚拟DOM增删改 + (diff对⽐)真实DOM差异化增删改 + (较少节点)回流/重绘;  //计算使⽤虚拟DOM的损耗
可以发现,都是围绕频繁操作真实DOM引起回流重绘,导致页⾯性能损耗来说的。不过框架也不⼀定⾮要使⽤虚拟DOM,关键在于看是否频繁操作会引起⼤⾯积的DOM操作。
那么虚拟DOM究竟通过什么⽅式来减少了页⾯中频繁操作DOM呢?这就不得不去了解DOM Diff算法了。
DIFF算法
当数据变化时,vue如何来更新视图的?其实很简单,⼀开始会根据真实DOM⽣成虚拟DOM,当虚拟DOM某个节点的数据改变后会⽣成⼀个新的Vnode,然后VNode和oldVnode对⽐,把不同的地⽅修改在真实DOM上,最后再使得oldVnode的值为Vnode。
diff过程就是调⽤patch函数,⽐较新⽼节点,⼀边⽐较⼀边给真实DOM打补丁(patch);
对照vue源码来解析⼀下,贴出核⼼代码,旨在简单明了讲述清楚,不然⼩编⾃⼰看着都头⼤了O(∩_∩)O
patch
那么patch是怎样打补丁的?
//patch函数 oldVnode:⽼节点 vnode:新节点
function patch (oldVnode, vnode) {
...
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode) //如果新⽼节点是同⼀节点,那么进⼀步通过patchVnode来⽐较⼦节点
} else {
/* -----否则新节点直接替换⽼节点----- */
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // ⽗元素
createEle(vnode) // 根据Vnode⽣成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, Sibling(oEl)) // 将新元素添加进⽗元素
oldVnode = null
}
}
...
return vnode
}
//判断两节点是否为同⼀节点
function sameVnode (a, b) {
return (
a.key ===
b.key && // key值
a.tag ===
b.tag && // 标签名
a.isComment ===
b.isComment && // 是否为注释节点
// 是否都定义了data,data包含⼀些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
从上⾯可以看出,patch函数是通过判断新⽼节点是否为同⼀节点:
如果是同⼀节点,执⾏patchVnode进⾏⼦节点⽐较;
如果不是同⼀节点,新节点直接替换⽼节点;
那如果不是同⼀节点,但是它们⼦节点⼀样怎么办嘞?OMG,要牢记:diff是同层⽐较,不存在跨级⽐较的!简单提⼀
嘴,React中也是如此,它们只是针对同⼀层的节点进⾏⽐较。
patchVnode
既然到了patchVnode⽅法,说明新⽼节点为同⼀节点,那么这个⽅法做了什么处理?
function patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el  //到对应的真实DOM
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return  //如果新⽼节点相同,直接返回
if ( !== null && !== null && !== ) {
//如果新⽼节点都有⽂本节点且不相等,那么新节点的⽂本节点替换⽼节点的⽂本节点
api.setTextContent(el, )
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
//如果新⽼节点都有⼦节点,执⾏updateChildren⽐较⼦节点[很重要也很复杂,下⾯展开介绍]
updateChildren(el, oldCh, ch)
}else if (ch){
//如果新节点有⼦节点⽽⽼节点没有⼦节点,那么将新节点的⼦节点添加到⽼节点上
createEle(vnode)
}else if (oldCh){
//如果新节点没有⼦节点⽽⽼节点有⼦节点,那么删除⽼节点的⼦节点
}
}
如果两个节点不⼀样,直接⽤新节点替换⽼节点;
如果两个节点⼀样,
新⽼节点⼀样,直接返回;
⽼节点有⼦节点,新节点没有:删除⽼节点的⼦节点;
⽼节点没有⼦节点,新节点有⼦节点:新节点的⼦节点直接append到⽼节点;
都只有⽂本节点:直接⽤新节点的⽂本节点替换⽼的⽂本节点;
都有⼦节点:updateChildren
最复杂的情况也就是新⽼节点都有⼦节点,那么updateChildren是如何来处理这⼀问题的,该⽅法也是diff算法的核⼼,下⾯我们来了解⼀下!
updateChildren
由于代码太多了,这⾥先做个概述。updateChildren⽅法的核⼼:
1. 提取出新⽼节点的⼦节点:新节点⼦节点ch和⽼节点⼦节点oldCh;
2. ch和oldCh分别设置StartIdx(指向头)和EndIdx(指向尾)变量,它们两两⽐较(按照sameNode⽅法),有四种⽅式
来⽐较。如果4种⽅式都没有匹配成功,如果设置了key就通过key进⾏⽐较,在⽐较过程种startIdx++,endIdx--,⼀旦StartIdx > EndIdx表明ch或者oldCh⾄少有⼀个已经遍历完成,此时就会结束⽐较。
下⾯结合图来理解:
第⼀步:
oldStartIdx = A , oldEndIdx = C;
newStartIdx = A , newEndIdx = D;
此时oldStartIdx和newStarIdx匹配,所以将dom中的A节点放到第⼀个位置,此时A已经在第⼀个位置,所以不做处理,此时真实DOM顺序:A  B  C;
第⼆步:
oldStartIdx = B , oldEndIdx = C;
newStartIdx = C , oldEndIdx = D;
此时oldEndIdx和newStartIdx匹配,将原本的C节点移动到A后⾯,此时真实DOM顺序:A  C  B;
第三步:
oldStartIdx = C , oldEndIdx = C;
newStartIdx = B , newEndIdx = D;
oldStartIdx++,oldEndIdx--;
oldStartIdx > oldEndIdx
此时遍历结束,oldCh已经遍历完,那么将剩余的ch节点根据⾃⼰的index插⼊到真实DOM中即可,此时真实DOM顺序:A  C B  D;
所以匹配过程中判断结束有两个条件:
oldStartIdx > oldEndIdx表⽰oldCh先遍历完成,如果ch有剩余节点就根据对应index添加到真实DOM中;
newStartIdx > newEndIdx表⽰ch先遍历完成,那么就要在真实DOM中将多余节点删除掉;
看下图这个实例,就是新节点先遍历完成删除多余节点:
最后,在这些⼦节点sameVnode后如果满⾜条件继续执⾏patchVnode,层层递归,直到oldVnode和Vnode中所有⼦节点都⽐对完成,也就把所有的补丁都打好了,此时更新到视图。
总结
最后,⽤⼀张图来记忆整个Diff过程,希望你能有所收获!
因为React只是简单学了基础,这⾥作为对⽐来概述⼀下:
1.React渲染机制:React采⽤虚拟DOM,在每次属性和状态发⽣变化时,render函数会返回不同的元素树,然后对⽐返回的元素树和上次渲染树的差异并对差异部分进⾏更新,最后渲染为真实DOM。
2.diff永远都是同层⽐较,如果节点类型不同,直接⽤新的替换旧的。如果节点类型相同,就⽐较他们的⼦节点,依次类推。通常元素上绑定的key值就是⽤来⽐较节点的,所以⼀定要保证其唯⼀性,⼀般不采⽤数组下标来作为key值,因为当数组元素发⽣变化时index会有所改动。
3.渲染机制的整个过程包含了更新操作,将虚拟DOM转换为真实DOM,所以整个渲染过程就是Reconciliation。⽽这个过程的核⼼⼜主要是diff算法,利⽤的是⽣命周期shouldComponentUpdate函数。
到此这篇带你搞懂Vue虚拟Dom与diff算法的⽂章就介绍到这了,更多相关Vue虚拟Dom与diff算法内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!