瞧一眼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就行了。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值