前言
使用过Vue和React的小伙伴肯定对虚拟Dom和diff算法很熟悉,它扮演着很重要的角色。由于小编接触Vue比较多,React只是浅学,所以本篇主要针对Vue来展开介绍,带你一步一步搞懂它。
虚拟DOM
什么是虚拟DOM?
虚拟DOM(Virtual Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的。 真实的元素节点:
1 2 3 <div id="wrap"> <p class="title">Hello world!</p> </div> VNode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { 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的作用是什么?
兼容性好。因为Vnode本质是JS对象,所以不管Node还是浏览器环境,都可以操作; 减少了对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, api.nextSibling(oEl)) // 将新元素添加进父元素 api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点 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必须相同 ) } |