前言
应用过Vue和React的小伙伴必定对虚构Dom和diff算法很相熟,它扮演着很重要的角色。因为小编接触Vue比拟多,React只是浅学,所以本篇次要针对Vue来开展介绍,带你一步一步搞懂它。
虚构DOM
什么是虚构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的作用是什么?
- 兼容性好。因为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必须雷同 )}
从下面能够看出,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 (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) }else if (ch){ //如果新节点有子节点而老节点没有子节点,那么将新节点的子节点增加到老节点上 createEle(vnode) }else if (oldCh){ //如果新节点没有子节点而老节点有子节点,那么删除老节点的子节点 api.removeChildren(el) } }}
如果两个节点不一样,间接用新节点替换老节点;
如果两个节点一样,
- 新老节点一样,间接返回;
- 老节点有子节点,新节点没有:删除老节点的子节点;
- 老节点没有子节点,新节点有子节点:新节点的子节点间接append到老节点;
- 都只有文本节点:间接用新节点的文本节点替换老的文本节点;
- 都有子节点:updateChildren
最简单的状况也就是新老节点都有子节点,那么updateChildren是如何来解决这一问题的,该办法也是diff算法的外围,上面咱们来理解一下!
updateChildren
因为代码太多了,这里先做个概述。updateChildren办法的外围:
- 提取出新老节点的子节点:新节点子节点ch和老节点子节点oldCh;
- 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;
参考vue实战视频解说:进入学习
第二步:
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中所有子节点都比对实现,也就把所有的补丁都打好了,此时更新到视图。
总结
dom的diff算法工夫复杂度为o(n^3),如果应用在框架中性能会很差。Vue应用的diff算法,工夫复杂度为o(n),简化了很多操作。
最初,用一张图来记忆整个Diff过程,心愿你能有所播种!
彩蛋
因为React只是简略学了根底,这里作为比照来概述一下:
1.React渲染机制:React采纳虚构DOM,在每次属性和状态发生变化时,render函数会返回不同的元素树,而后比照返回的元素树和上次渲染树的差别并对差别局部进行更新,最初渲染为实在DOM。
2.diff永远都是同层比拟,如果节点类型不同,间接用新的替换旧的。如果节点类型雷同,就比拟他们的子节点,顺次类推。通常元素上绑定的key值就是用来比拟节点的,所以肯定要保障其唯一性,个别不采纳数组下标来作为key值,因为当数组元素发生变化时index会有所改变。
3.渲染机制的整个过程蕴含了更新操作,将虚构DOM转换为实在DOM,所以整个渲染过程就是Reconciliation。而这个过程的外围又次要是diff算法,利用的是生命周期shouldComponentUpdate函数。