vue-diff算法

1. 虚拟DOM

什么是虚拟DOM?

虚拟DOM说到底,就是用JS去描述一段 html代码,比如:

<li key="li">
	Virtual DOM
</li>

上面这一段代码,用虚拟DOM表示的话就是:

{
    tag: "li",
    text: "Virtual DOM",
    key: "li",
    ....
}

来段稍微复杂一点的:

<div id="div1" class="container">
    <ul style="font-size: 28px">
        <li>a</li>
    </ul>
</div>

js来描述就是:

{
    tag: "div",
    props: {
        id: "div1",
        className: "cotainer"
    },
    children: {
        tal: "url",
        props: {
            style: "font-size: 20px"
        },
        children: {
            tag: "li",
            children: "a"
        }
    }
}
为什么要有虚拟DOM(优点)

首先,如果你能保证你总是能精确地操作DOM,那么直接操作DOM能带来最好的性能,但是每次都直接去操作DOM,一方面心智成本太高,另一方面代码也不易维护。

所以就有了虚拟DOM的概念,它本质上是用JS对象去描述一个DOM节点,相当与对真实DOM的一层抽象。

优点有:

  • 能保证一定的性能下限,能够保证在不手动优化的情况下,提供还算可以的性能,总比粗暴地去操作DOM性能好一些;
  • 不用手动操作DOM,能够提高开发效率。
  • 能支持跨平台,因为它是用JS对象来抽象描述真实DOM,所以当比如服务端渲染SSR的时候,就可以使用虚拟DOM来完成。
虚拟DOM真的比真实DOM性能好吗(缺点)

当然是不一定,直接操作DOM速度肯定是最快的。

  • 虚拟DOM虽然能保证一定程度的性能下限,但是无法做到极致的性能优化;
  • 首次大量渲染DOM时,因为多了一层虚拟DOM的计算,所以速度会慢一些。

2. diff算法

有了虚拟DOM,那这个虚拟DOM和真实DOM之间的一个比较过程是怎样的呢?这便是diff算法的用途了。

2.1 比较策略

首先,先从宏观上把握一下diff算法的一个策略,策略是这样的:

  1. 同层比较

也就是新旧节点在进行比较的时候,只会在同级间比较,不会跨级比较。

  1. 如果 tag 不同,则直接删掉重建,不再深度比较。
  2. 同时比较 tagkey,如果两者都相同,则认为是相同节点,不再深度比较。
2.2 流程概述

diff算法是组件patch()更新的一个过程,首先会判断是否为同一个标签(tag)

  1. 如果不是,那么就直接用新的vnode替换旧的vnode
  2. 如果是就进入patchVNode阶段,先判断是否为文本节点:
    1. 是文本节点的话,就直接判断文本是否相同,不同就直接替换文本;(文本节点没有子元素)
    2. 不是文本节点的话,就判断是否有子节点:
      1. 如果旧的vnode有,而新的vnode没有,就需要删除旧节点下的DOM
      2. 如果旧的vnode没有,而新的vnode有,就需要创建新的DOM
        1. 如果都有子节点,就需要进行子节点的比较(通过updateChildren)这是diff算法比较关键的一点,后面介绍updateChildren函数会说到。

在这里插入图片描述

以上过程中,在vue中,主要是通过patch()patchVnode()updateChildren几个函数来实现的,接下来,就把这几个函数都仔细介绍下。

2.3 具体分析
2.3.1 前置知识
isDef()isUndef()

这两个函数是用来判断vnode是否存在的,而vnode本质上又是个JS对象,所以实际这俩函数就用来判断vnode是不是undefined或者null

sameVnode()

在源码中,sameVnode用来判断两个节点是否为相同节点。那么相同节点的判断依据是什么呢?在vue中,判断一句便是keytag等静态属性值是否相等。

在此需要注意一点:相同节点不代表是相等节点。相同节点是通过vnode1 === vnode2来判断的。举个例子:<div>hello</hello><div>hi</div>是相同节点,而非相等节点。

2.3.2 patch()

patch()函数是对新旧vnode做一个简单的判断,还没有进入详细的比较阶段。

  • 首先,判断vnode是否存在,如果不存在,就代表整个旧节点应该删除;
  • 如果vnode存在的话,再判断oldVnode是否存在,如果不存在,说明需要新增整个vnode
  • 如果vnodeoldVnode都存在,就需要判断二者是否为相同节点,如果是的话,就调用patchVnode()方法。
  • 如果二者不是相同节点,那么这种情况一般是初始化页面。
2.3.3 patchVnode()

patchVnode()中,就开始对新旧两个vnode进行比较了。

  • 首先判断oldVnodevnode是否是相等节点。是的话就结束函数的执行。
  • 更新节点属性。
  • 接着判断vnode是否为文本节点,即判断vnode.text是否存在,如果存在,即vnode是文本节点,只需要更新节点文本即可。
  • 如果不是文本节点,继续判断oldVnodevnode是否有子节点:
    • 如果两者都没有子节点,就判断oldVnode.text是否有内容,有内容清空即可;
    • 如果vnode有子节点,而oldVnode没有,则新增所有的子节点;
    • 如果vnode没有子节点,而oldVnode有,则需删除所有的子节点。
    • 如果两者都有子节点,就进入到updateChildren()函数进行下一步比较并更新孩子节点。
2.3.4 updateChildren(oldCh, newCh)

重头戏来了,这里是最关键的一步,也是比较难理解的一步。

首先,这个方法有两个重要的参数:oldChnewCh,这两个参数都是一个数组,前者为oldVnode的子节点;后者为vnode的子节点。

总的来说,这个方法的作用,就是对两个数组中的子节点进行一一比较,找到相同的节点再进行patchVnode()去比较更新;而剩下的呢,就根据实际情况进行删除或新增。(比如两个数组经过比较后,newCh中还有多余的节点,那就说明这是需要新增的节点;如果oldCh中还有多余的节点,那就说明这是需要删除的节点)。

对于这个需求,一般人想到的便是直接循环遍历两个数组,进行一一对比,但很显然,vue不会这么做,肯定有更优雅,复杂度更小的做法。

vue中,是通过四个指针来实现的,这四个指针分别设为两个数组的头尾,下文用旧头、旧尾、新头、新尾来表示。接下来说说,vue是如何利用这四个指针来实现两个数组的对比。

首先是旧头—新头进行比较,如果二者相同,就对二者执行patchVnode(),并向中间移动这两个指针。

如果旧头—新头不相同呢?就对旧尾—新尾进行比较。

如果旧头—新头,旧尾—新尾都不相同,那就进行交叉比较,首先是旧头—新尾比较:

如果二者相同,那么就执行patchVnode(),并把旧头移动到尾部。

如果旧头—新尾不相同,那就比较新头—旧尾:

如果二者相同,那么就执行patchVnode(),并把旧尾查到头部。

如果交叉比较也都匹配不上的话,那就要进行暴力对比了:也就是针对新头这个节点,去遍历oldCh中的所有节点,来进行一一匹配。

值得注意的是,在暴力对比之前,需要先生成一个oldChkey-->index的映射表,在一一对比的时候,直接拿新头的key值去和这个映射表对比就好了;如果节点没有key值呢?就会默认undefinedkey值的重要性体现出来了)

暴力对比一轮下来,如果可以找到相匹配的那么就对新头进行移动操作,如果找不到就直接将新头插入。

这里有个细节需要注意:当暴力比对匹配时,需要将oldCh所匹配的节点置为undefined,这样指针移动的时候就可以跳过这个节点。

那么到此为止,updateChildren()这个方法就讲完了,总体概括起来就是:

  1. 设置新旧首尾指针;
  2. 旧头新头比较;
  3. 旧尾新尾比较;
  4. 旧头新尾比较;
  5. 旧尾新头比较;
  6. 交叉比较;
  7. 暴力比较。
2.4 key值的重要之处

通过上面的 diff算法的描述,我们可以知道key值在两个地方扮演着重要的角色。

  1. 首先是在sameNode()中,判读新旧两个节点是否相同,需要用的key值;
  2. 在交叉比较不匹配,需要暴力对比时,是需要用的oldChkey-->index映射表的key值来循环比较。
  • 22
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Joyce Lee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值