瞧一眼vue2.0的虚拟DOM
Posted 登楼痕
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了瞧一眼vue2.0的虚拟DOM相关的知识,希望对你有一定的参考价值。
虚拟DOM是什么?为啥要使用它呢?
说白了就是把真实DOM转换为一定数据结构的js对象存储。这样做的原因很大程度是因为真实DOM是很复杂的结构,每次视图变化如果直接操作修改DOM,其复杂度和性能开销非常大,但是计算js可快多了,所以何不将DOM转换为js对象进行比对,然后根据计算出来的新旧DOM的差异有针对性地进行修改。并且虚拟DOM还可以维护程序的状态,即记录和跟踪上一次DOM的状态。 vue2.0的虚拟DOM就是参考改造了snabbdom.js(目前最快的虚拟dom库之一),所以就大致瞅瞅几百行源代码的snabbdom.js是怎么实现这一系列操作的。 在了解原理之前先看看Snabbdom提供的几个核心的函数: 1、init():一个高阶函数,返回一个函数patch(); 2、大家眼熟的h():返回VNode,用来创造虚拟DOM的; 3、thunk(): 一种优化策略,处理不可变数据时使用; 4、patch(): 核心的函数,比较新旧虚拟DOM差异从而更新真实DOM的。 h()函数的调用例如:let vNode = h('div#myId.myClass','hahahahah') 或者 let vNode = h('div#myId.myClass', [h('a',class:'class1':true,'xxx'), ...]) 或者 let vNode = h('div#myId.myClass',style:color:'red', [h('a',class:'class1':true,'xxx'), ...])
很明显,h()函数是一个重载函数,可以接收不同个数和类型的参数。
细节先不看,就先了解最基本的几个问题:
1、虚拟DOM怎么生成的?也就是h()函数到底做了些什么;
2、新旧虚拟DOM是怎么对比差异的,也就是patch()做了什么;
3、得到的差异是怎么渲染更新真实DOM的。
一、h()函数
大致瞅瞅h()函数(在h.ts文件中定义),其实就是有4种调用的方式,因为一个DOM节点会有自己的tag、id、class,以及额外的属性比如style那些,还会有文本节点或者子DOM节点。所以h()函数里的各种if/else就是将这些情况依次分类整理出sel、data、children、text这些,最后统一调用vnode()函数来生成虚拟DOM的js对象。
return vnode(sel, data, children, text, undefined);
二、vnode()函数
vnode()函数就很简单了,把h()函数整理的参数返回对应的js对象而已:
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode
const key = data === undefined ? undefined : data.key;
return sel, data, children, text, elm, key ;
三、patch()函数
得到了vNode对象,那么就要通过patch()对比新旧差异来更新DOM。
主要思路就是:
1、通过key和sel属性是否都想等判断是不是相同节点,是的话就继续去寻找二者差异(patchVnode()做的事情);
2、不是相同节点,就先拿旧节点的父节点,然后把新节点转换为DOM(createElm()做的事情)绑定在旧节点的父节点上,删除旧节点【替换操作】
3、前后都会执行一些钩子函数。
四、patchVnode()函数
传统的diff算法复杂度是O(n^3),看起来也很费力,针对相同组件的dom结构是一致的,并且同一层级的一组节点可以通过key区分,那么虚拟DOM的Diff算法只比较相同层级的dom,不会跨层比较,如果发生div变成p标签这种情况,就整个子树进行替换,这样就将复杂度降到了O(n)。
既然只比较同层级的,那思路就很明确,判断同层级不同情况做替换更新+children递归:
1、新旧相同,啥都不干直接return结束;
2、新旧节点均有text并且二者不相同:如果老节点还有children,那就干掉后替换为新节点的text;
3、新、旧节点都有children且二者不相同:调用updateChildren()对自己进行递归,对比子节点,然后更新差异;
4、新的有children,旧的没有:旧的如果有text,就干掉后加上新的children;
5、新的没有,旧的有children:干掉children就行了;
6、新的没有text,旧的有text:干掉text就行了。
五、updateChildren()函数——diff算法
对于同层子节点,通过删除、新建、移位的方法,最大程度复用老的dom,算法维护了四个索引:
oldStartIdx => 旧头索引
oldEndIdx => 旧尾索引
newStartIdx => 新头索引
newEndIdx => 新尾索引
然后一个while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)开始逐一比较新旧子节点,直到任一一组子节点遍历完毕。
首先用sameVnode()对进行首位新旧节点的两两比较:
1、oldStart和newStart比较,如果相似,不需要执行移动操作,则直接patchVnode(),然后就到下一对新旧节点;
2、同上,oldEnd和newEnd比较,相似则不需要执行移动操作,就patchVnode()然后都前移索引;
3、oldStart和newEnd比较,如果相似,证明这个节点是在新dom里末尾去了,所以patch完后还要进行移动操作,就把当前这个节点移动到oldCh所有未处理节点的最后面,已处理节点的前面,只有这样才能和newEnd的位置相同,然后oldStart索引后移,newEnd索引前移;
4、同理3,oldEnd和newStart处理类似,是一个左移操作。
如果上面4种情况都不满足,那么就通过key-index映射来最大程度复用旧节点,如果新节点在旧节点中不存在,就插入;如果存在,就更新,还要把旧节点对应位置设为undefiend代表这个节点已经被处理且移到了其他位置,避免重复处理(也就是函数一开始的4个null判断的if语句,来判断一下节点是不是在之前循环中移动过来的,是的话就跳过)。旧的循环完了,新的里面的未处理的节点全部加进最后一个新节点之后;新的循环完了,那么旧的里面剩下的都是要删的。
六、createElm()函数
函数名就可以看出,这个是根据vnode生成真实DOM的,其实也就是h函数的反向操作。利用封装的appendChild,insertBefore等函数,将tag,注释节点,文本内容,id,class还原,然后对children进行递归调用createElm再appendChild就行了。
以上是关于瞧一眼vue2.0的虚拟DOM的主要内容,如果未能解决你的问题,请参考以下文章