• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Vue3-优化提升

武飞扬头像
sunsetFeng
帮助145

一、简要介绍

在Vue3整个框架中,主要分为runtime以及complier两个部分。

  • complier 负责编译解析
  • runtime 提供运行时环境

两部分有着极为紧密的前后关联关系。complier解析数据并提供有效信息给runtime,促使runtime能更高效的运行。

二、传统的diff算法

diff算法直译过来就叫做差异更新算法。Vue2和Vue3在差异更新上存在着很大的不同,Vue2的diff算法在下文统称为传统的diff算法,下面简要介绍这种diff算法的思想。

1、算法逻辑

传统的diff算法的核心逻辑就是全量比较。假如存在如下模板:

<div>
  <span>测试</span>
  <i>斜体</i>
  <span>{{ data }}</span>
</div>

data数据变更后,会触发模板重新渲染,此时进行差异更新,更新步骤如下:

编译优化-图1.png

这种模式的表现就是从dom树的根节点开始比较,如果某个元素变更,则更新这个元素,直到完成整个比较流程。

2、存在的问题

虽然传统的diff算法能够准确的完成更新操作,但其中有着许多不需要的比较操作。比如上图中的1,2,3步骤就是无效操作,因为这些元素都是静态的,不可能更改的。

当一个模板静态内容比较多,使用这种全量的比较更新,就会大大的浪费的性能,因此Vue3使用了全新的diff算法,下文统称为优化的diff算法

三、虚拟节点

diff算法的目的是进行比较,然后进行差异更新。在实际过程中,是没有直接使用dom元素进行比较的,而是有一棵虚拟节点树,通过比较树上的节点,最终映射到dom树上。

因此在聊优化的diff算法前,先说说虚拟节点中的几个关键节点。

1、虚拟节点的作用

虚拟节点树本质上是dom树的映射。每一个dom元素,都可以找到与之对应的虚拟节点,但虚拟节点树是要比dom树更大的,因为有些虚拟节点并没有被转换为dom元素,它们是一种“功能节点”。

2、Block节点与Block Tree

在了解虚拟节点与dom元素之间的关系之后,我们来接下来谈谈虚拟节点的另外一重身份,Block节点。一个虚拟节点,可能是一个Block节点。它们的区别如下:

  • 普通虚拟节点:

    const node:VNode = {
      type:'div',
      children:[
        { type:'span', children:'测试'},
        { type:'i',children:'斜体'},
        { type:'span',children:ctx.data}
      ]
    }
    
  • Block节点:

    const node:VNode = {
      type:'div',
      children:[
        { type:'span', children:'测试'},
        { type:'i',children:'斜体'},
        { type:'span',children:ctx.data, patchFlag:1 /**动态的文本**/}
      ],
      dynamicChildren:[
        { type:'span',children:ctx.data,patchFlag:1}
      ]
    }
    

可以看出,Block节点比一个普通虚拟节点多了一个关键属性dynamicChildren,这个属性用于存放所有的动态节点。传统的diff算法会进行全量比较,那么怎么去进行优化呢?最理想的方式就是把会变化的节点收集起来,每次只比较这部分节点即可,这种方式直接过滤掉了静态节点,极为高效。因此在执行diff算法时,就可以直接比较dynamicChildren里面的节点,这样极大的减少了比较的次数。

当然,dynamicChildren节点里面不仅要存放动态节点,还要存放Block节点,这样就可以构成如下结构:

- Block(div)
    - VNode(span)
    - Block(div)
        - VNode(i)
        - VNode(span)
    - VNode(span)

这样就可以构成一棵Block Tree,通过比较Block Tree进行差异更新,就是优化的diff算法的核心思想,只比较动态节点,跳过静态节点

3、Fragment节点

Block节点的介绍告一段落,现在介绍另一个特殊节点,Fragment节点。Fragment节点是虚拟节点的一种,它是一种功能节点,不会生成任何dom元素。这个节点是一组虚拟节点的逻辑父级,当渲染这个节点时,会直接渲染其子节点。这个表述可能不大容易理解,我用下面的例子来阐述其作用。

  • 实现多根节点

    假如一个组件存在如下模板:

    <template>
      <div></div>
      <div></div>
    </template>
    

    在Vue2中,这种模板是无法通过编译的。Vue2解析这种模板会生成一棵多根树,但Vue2只支持单根节点。但在Vue3中,这种模板是支持的,这并不是说Vue3支持多根节点,而是Vue3会把这种多根模板转换为单根节点的虚拟节点树,其根节点就是Fragment节点。结构如下:

    - VNode(Fragment)
        - VNode(div)
        - VNode(div)
    
  • 维持Block Tree的稳定树结构

    假如一个组件存在如下模板:

    <template>
      <div>
        <i>{{ item.length }}</i>
        <div v-for="item in items">
          <span>{{item.name}}</span>
        </div>
        <div>{{ name }}</div>
      </div>
    </template>
    

    通过前面的讲解,这个模板最终会解析为如下Block Tree:

    - Block(div)
        - VNode(i)
        - VNode(span)
        ... // 动态节点span的数量根据items的大小确定
        - VNode(span)
        - VNode(div)
    

    假如items的数据数量变化,那么变更后的Block Tree结构和之前的Block Tree结构就无法匹配,但优化的diff算法必须保证Block Tree变更前后保持一致的树结构,只有这样才能进行快速的节点比较。因为Block节点收集动态节点是跨越层级的(比如上例中v-for指令所在的div节点没被收集,其子节点span节点被收集),当树的层级结构不一致时,无法确定删减或新增的节点

    那么转换后的Block Tree如下:

    - Block(div)
        - VNode(i)
        - Block(Fragment) //它是Block节点,不管items的大小是怎样的,这儿都是1个Fragment节点
            - VNode(span)
            ... //根据items的大小确定
            - VNode(span)
        - VNode(div)
    

4、生成Block节点的情况

在上文已经说过,通过比较Block Tree就是实现优化的diff算法的核心思想,那么接下来就聊聊什么情况下会生成Block节点。

本质上所有能导致虚拟节点树不稳定的地方,都需要生成Block节点,外加组件根节点必须是Block节点来作为Block Tree的根节点。主要如下:

  • 根节点

    根节点必须是一个Block节点,因为一棵Block Tree必须得有一个节点作为初始节点,这个初始节点就是根节点。

  • Fragment节点

    前文说过,Fragment节点是功能节点,不渲染任何实体的dom元素。它就像是一个节点容器一样,将一组不稳定渲染的节点封装为一个容器节点,以便于维持Block Tree的稳定性。

    但稳定也是相对的,我还是用上面介绍Fragment节点时的例子来说明,针对以下模板:

    <template>
      <div>
        <i>{{ item.length }}</i>
        <div v-for="item in items">
          <span>{{item.name}}</span>
        </div>
        <div>{{ name }}</div>
      </div>
    </template>
    

    如果没有Fragment,那么Block Tree将不稳定,因此无法快速比对节点。其实即便有了Fragment节点,Block Tree在结构上也不是完全稳定的,示例Block Tree如下:

    - Block(div)
        - VNode(i)
        - Block(Fragment)
            - VNode(span)
            ... //根据items的大小确定
            - VNode(span)
        - VNode(div)
    

    Fragment节点的子节点数量不固定,因此Fragment节点内部是不稳定的。但对于整棵Block Tree而言,只要把Fragment节点视为一个节点而不考虑其内部变化,那么Block Tree就是稳定的。

    其实Fragment节点不全是不稳定的,也存在稳定的Fragment节点,比如以下模板:

     <template>
       <div>
         <div v-for="num in 5">
           <span>{{ items[num].name }}</span>
         </div>
       </div>
     </template>
    

    这种v-for指令生成的Fragment节点永远都只有5个子节点,因此便称之为稳定的Fragment节点。

    稳定的Fragment节点不稳定的Fragment节点在执行diff算法时存在很大的区别。当比对2个Fragment节点时,本质是比较其子节点。

    • 稳定的Fragment

      稳定的Fragment节点有着稳定个数的子节点,可以采用优化的diff算法来快速比对。

    • 不稳定的Fragment

      不稳定的Fragment节点有着不稳定个数的子节点,只能采用传统的diff算法来比对。

  • v-if v-else v-else-if

    条件指令会直接导致某个节点不被生成,这会破坏Block Tree的稳定性,因此也会生成Block节点,针对如下模板:

    <template>
      <div>
        <span v-if="show"></span>
      </div>
    </template>
    

    当满足条件时很好理解,Block Tree如下:

      - Block(div)
       - Block(span)
    

    当不满足条件会生成一个注释节点用于保证Block Tree的稳定性,注释节点不会渲染任何dom元素,就是一个功能性节点,如下:

      - Block(div)
       - Block(comment)
    

    其实条件指令不仅仅是单分支渲染会破坏Block Tree的稳定性。在多分支渲染时,每个分支的结构也可能不一样,这也会破坏Block Tree的结构稳定性。因此vue-sfc模块在编译条件指令时,都会给这些节点添加一个key用于保证不同分支节点是唯一的。key是节点唯一标识,一旦key不一样,那么就会认为是不一样的节点。

    简要解析代码如下:

    image.png

  • 动态key绑定的节点

    上面提到,key是节点的唯一标识。即便节点的所有属性的一致,但其key不一样,那么这也是不一样的节点。当动态key绑定的节点不作为一个Block节点时,那么其子节点中的动态节点就会被上层Block节点收集,这是不对的。因为一旦key变化,这个节点及其子节点就会被卸载然后重建,一旦被上层收集就会引发异常。

    其实动态key绑定的节点和条件指令的原理很相似,条件指令本身也有利用key来保证分支的唯一性。

四、PatchFlags

前文主要都是针对Block Tree在进行描述。Block Tree的目的是减少比较VNode的次数,尽量只比较动态节点。但每个节点都很有多属性,如果能够快速定位每个节点变更的属性,那么针对渲染优化而言,也有很大帮助。

在Vue3中,complier模块在编译模板时,会分析模板中所有的动态属性,并且对这些动态节点赋予一个PatchFlag,当在进行更新时,只需要去判断PatchFlag,便能快速的进行更新操作。

下面是针对PatchFlag的简单总结:

export const enum PatchFlags {
  // 动态的文本,只需要比较文本内容
  TEXT = 1,
  // 动态的class,只需要比较class
  CLASS = 1 << 1,
  // 动态的样式,只需要比较样式
  STYLE = 1 << 2,
  // 动态的属性值,只需要比较指定属性  :name="name"
  PROPS = 1 << 3,
  // 动态的属性,比较全部属性   :[prop]="prop"
  FULL_PROPS = 1 << 4,
  // 自定义的事件
  HYDRATE_EVENTS = 1 << 5,
  // 稳定的Fragmengt
  STABLE_FRAGMENT = 1 << 6,
  // 有key的Fragment
  KEYED_FRAGMENT = 1 << 7,
  // 没有key的Fragment
  UNKEYED_FRAGMENT = 1 << 8,
  // 没有属性变化的更新  比如ref,每一次更新完成后都需要设置ref,ref可能因为其他值的影响而变化
  NEED_PATCH = 1 << 9,
  // 动态的插槽
  DYNAMIC_SLOTS = 1 << 10,
  // 开发模式使用 不考虑
  DEV_ROOT_FRAGMENT = 1 << 11,
  // 静态提升的节点
  HOISTED = -1,
  // 节点必须走传统diff,不能使用优化模式
  BAIL = -2
}

一旦给某个节点打上了PatchFlag,那么在更新时,便可以根据PatchFlag去快速的更新,而不用所有的属性一一比较。

五、静态提升

静态提升很好理解,在执行render函数时,会动态的生成VNode,但许多节点是静态节点,在更新时没有必要再次创建,因此将这部分节点保存起来二次复用,就是静态提升。简单示意如下:

image.png

静态提升功能在遇到大批量静态模板存在时,还会将其自动预字符串化

image.png

当遇到大量连续静态模板时,会将这些静态模板拼接成html,这样会大大减少生成的VNode数量。

六、事件缓存

当在开发时,我们常常不经意间会写出内联的事件,比如:

<div @click="() => {}"></div>

每当更新重新执行render函数时,这个内联的匿名函数都会被执行,这样会造成内存的浪费。在Vue3中,会将这种内联事件进行缓存,避免二次创建。

七、总结

Vue3的优化提升模块总结完毕,其实相较于Vue2而言,优化的地方还有更多,这篇博客主要侧重于编译优化的方面。在响应式等方面也有使用Proxy代理来进行性能优化,更有着依赖收集相关的优化算法更新。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanfbegb
系列文章
更多 icon
同类精品
更多 icon
继续加载