吃透Vue虚拟Dom和diff算法
前言用过Vue和React的朋友一定对虚拟Dom和diff算法不陌生,这起着非常重要的作用。由于小编对Vue的接触比较多,而React只是粗浅的学习,所以本文主要介绍Vue,带大家一步步了解。虚拟DOM什么是虚拟DOM?虚拟DOM(VirtualDOM),也就是我们常说的虚拟节点,是使用JS对象来模拟真实DOM中的节点。该对象包含真实DOM的结构和属性,用于比较虚拟DOM与真实DOM的区别,从而进行局部渲染,达到优化性能的目的。真实元素节点:Helloworld!
VNode:{tag:'div',attrs:{id:'wrap'},children:[{tag:'p',text:'Helloworld!',attrs:{class:'title',}}]}为什么要使用虚拟DOM?简单了解虚拟DOM后,可能有朋友会问:为什么在Vue和React框架中使用它?好问题!然后解决小伙伴们的疑惑。起初,我们在使用JS/JQuery时,不可避免地要对DOM进行大量操作,DOM的变化会触发回流或重绘,从而降低页面渲染性能。那么如何减少对DOM的操作呢?这时候就诞生了虚拟DOM应用,所以虚拟DOM的主要目的就是减少频繁操作DOM带来的回流和重绘带来的性能问题!虚拟DOM的作用是什么?良好的兼容性。因为Vnode本质上是一个JS对象,无论是Node还是浏览器环境都可以操作;减少了对Dom的操作。页面中的数据和状态变化都是通过Vnode进行比较,比较后只需要更新DOM,无需频繁操作,提高了页面性能;虚拟DOM和真实DOM有什么区别?说到这里,虚拟DOM和真实DOM有什么区别?总结大致如下:虚拟DOM不会进行回流和重绘;真实DOM由于频繁操作导致回流和重绘导致性能低下;virtualDOM频繁修改,然后一次性比较差异并修改realDOM,最后在sequenceDrawing中回流重绘,减少了realDOM中多次回流重绘带来的性能损失;虚拟DOM有效的减少了大面积的重绘和排版,因为它是和真实的DOM比较,更新的是差异,所以只渲染部分;totalloss=真实DOM增删改+(多节点)回流/重绘;//计算使用真实DOM的总损失=虚拟DOM增删改+(diff比较)真实DOM差分增删改+(少节点)回流/重绘;//计算使用虚拟DOM的损失可以发现,都是围绕真实DOM的频繁操作造成回流和重绘,导致页面性能损失。然而,框架并不一定要使用虚拟DOM。关键看频繁操作会不会引起大规模的DOM操作。那么虚拟DOM是通过什么方式来减少页面中DOM的频繁操作呢?这就得了解DOMDiff算法了。DIFF算法vue如何在数据变化时更新视图?其实很简单。一开始,虚拟DOM会基于真实DOM生成。当虚拟DOM中某个节点的数据发生变化时,会产生一个新的Vnode。然后,VNode会和oldVnode进行比较,不同的地方会在真实的DOM上进行修改。oldVnode的值为Vnode。diff过程就是调用patch函数,比较新老节点,边比较边打补丁真实DOM;对照vue源码分析,贴出核心代码,力求解释清楚,不然小编自己看。头大O(∩_∩)Opatch,那么patch是怎么打补丁的呢?//patchfunctionoldVnode:oldnodevnode:newnodefunctionpatch(oldVnode,vnode){...if(sameVnode(oldVnode,vnode)){patchVnode(oldVnode,vnode)//如果新旧节点相同node,then通过patchVnode进一步比较子节点}else{/*-----否则,新节点直接替换旧节点-----*/constoEl=oldVnode.el//对应的真实元素节点当前oldVnodeletparentEle=api.parentNode(oEl)//父元素createEle(vnode)//根据Vnode生成新元素if(parentEle!==null){api.insertBefore(parentEle,vnode.el,api.nextSibling(oEl))//添加新的元素添加到父元素中api.removeChild(parentEle,oldVnode.el)//移除旧的旧元素节点oldVnode=null}}...returnvnode}//判断是否两个节点是同一个节点functionsameVnode(a,b){return(a.key===b.key&&//keyvaluea.tag===b.tag&&//tagnamea.isComment===b.isComment&&//是否为注释节点//是否全部定义了data,data包含了一些具体的信息,比如onclick,styleisDef(a.data)===isDef(b.data)&&sameInputType(a,b)//当标签为
时,类型mustbethesame)}从上面可以看出,patch函数判断新旧节点是否是同一个节点:如果是同一个节点,则执行patchVnode比较子节点;如果不是同一个节点,则新节点直接替换旧节点;如果不是同一个节点,如果是同一个子节点怎么办?天哪,记住:diff是同级比较,没有跨级比较!简单的说,React中也是一样,只是针对同一层的节点进行比较现在patchVnode已经到了patchVnode方法,说明新旧节点是同一个节点,那么这个方法是做什么的呢?functionpatchVnode(oldVnode,vnode){constel=vnode.el=oldVnode.el//找到对应的真实DOMleti,oldCh=oldVnode.children,ch=vnode.childrenif(oldVnode===vnode)return//如果新旧节点相同,直接返回if(oldVnode.text!==null&&vnode.text!==null&&oldVnode.text!==vnode.text){//如果新旧节点有文本节点和不相等,则新节点的文本节点替换旧节点的文本节点api.setTextContent(el,vnode.text)}else{updateEle(el,vnode,oldVnode)if(oldCh&&ch&&oldCh!==ch){//如果新旧节点都有子节点,执行updateChildren比较子节点【很重要也很复杂,下面介绍一下】updateChildren(el,oldCh,ch)}elseif(ch){//如果新节点有子节点,旧节点没有子节点,则将新节点的子节点添加到旧节点createEle(vnode)}elseif(oldCh){//如果新节点节点没有子节点和旧的node有子节点,则删除旧节点的子节点api.removeChildren(el)}}}如果两个节点不一样,直接用新节点替换旧节点;如果两个节点相同,新旧节点相同,直接返回;旧节点有子节点,新节点没有:删除旧节点的子节点;旧节点无子节点,新节点有子节点:新节点的子节点直接追加到旧节点上;两者都只有文本节点:直接用新节点的文本节点替换旧文本节点;有子节点:updateChildren最复杂的情??况是新旧节点都有子节点,那么updateChildren是如何处理这个问题的呢?这个方法也是diff算法的核心。下面就来一探究竟吧!updateChildren由于代码太多,这里简单概括一下updateChildren方法的核心:提取新旧节点的子节点:新节点子节点ch和旧节点子节点oldCh;ch和oldCh分别设置了StartIdx(指向head)和EndIdx(指向tail)变量,它们成对比较(按照sameNode方法),有四种比较方式。如果四种方法都没有匹配成功,如果设置了key,则按key进行比较。比较过程中,startIdx++,endIdx--,一旦StartIdx>EndIdx表示至少遍历了ch或oldCh其中之一,此时比较结束。下面结合图来理解:Step1:oldStartIdx=A,oldEndIdx=C;newStartIdx=A,newEndIdx=D;此时oldStartIdx和newStarIdx匹配,所以将dom中的A节点放在第一个位置,而此时A已经在第一个位置,所以不处理。这时候真正的DOM顺序:ABC;参考vue实战视频讲解:进入第二步学习:oldStartIdx=B,oldEndIdx=C;newStartIdx=C,oldEndIdx=D;此时oldEndIdx和newStartIdx匹配,把原来的C节点移到A后面,此时真实的DOM顺序:ACB;第三步:oldStartIdx=C,oldEndIdx=C;newStartIdx=B,newEndIdx=D;oldStartIdx++,oldEndIdx--;oldStartIdx>oldEndIdx此时遍历结束,oldCh已经遍历完毕,将剩余的ch节点插入到真正的DOM根据自己的索引。这时候真正的DOM顺序:ACBD;所以判断匹配过程结束有两个条件:oldStartIdx>oldEndIdx表示oldCh先完成遍历,如果ch还有剩余节点,则根据对应的索引添加到真实DOM中;newStartIdx>newEndIdx表示ch已经遍历完了,那么要删除realDOM中多余的节点;见下图示例,即新节点首先遍历并删除冗余节点:最后,在这些子节点sameVnode之后,如果满足条件,继续执行patchVnode,递归,直到oldVnode中的所有子节点和Vnode进行对比,所有的补丁都搞定了,此时更新到视图中。综上所述,DOM的diff算法的时间复杂度为o(n^3),如果用在框架中性能会很差。Vue使用的diff算法时间复杂度为O(n),简化了很多操作。最后用一张图记住整个Diff过程,希望大家有所收获!彩蛋由于React只是简单的学习了基础,这里做一个概述以供对比:1.React渲染机制:React使用虚拟DOM,render函数会在每次属性和状态变化时返回不同的元素树,然后对比一下返回的元素树和上次渲染的树之间的差异被更新,最终被渲染为真正的DOM。2.diff始终是同层比较,如果节点类型不同,直接用新旧替换。如果节点属于同一类型,则比较它们的子节点,依此类推。通常,元素绑定的键值用于节点比较,因此必须保证其唯一性。一般不使用数组下标作为键值,因为索引会随着数组元素的变化而变化。3.渲染机制的整个过程包括一个update操作,将虚拟DOM转化为真实DOM,所以整个渲染过程就是Reconciliation。这个过程的核心主要是diff算法,使用了生命周期shouldComponentUpdate函数。