瞧一眼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的主要内容,如果未能解决你的问题,请参考以下文章

Vue2.0 瞧一下vm.$mount()

Vue2.0 瞧一下vm.$mount()

Vue2.0 瞧一下vm.$mount()

vue2.0框架认识

Vue源码学习之虚拟DOM——Vue中的DOM-Diff (上)

vue2.0之render函数