vue3的改变(hoist 与 Block)

708 阅读4分钟

Vue3在模板编译diff算法方面作了许多优化,最近我就了解到了静态节点作用域提升(hoist)以及利用BLockTree来优化diff算法。下面我来分别大概介绍这两种优化的具体实现。

了解到的知识来自于《vuejs设计与实现》

静态节点作用域提升

我们知道,在挂载时,会调用组件实例上的render函数来获取组件对应的vnode。在更新时也会重新调用render函数来获取到新的vnode,然后对新旧vnode进行patch(diff算法)操作。

假设我们有下面这个组件

image.png

转化后的render函数如下,可以看到,静态节点<div>jzsp</div>的vnode作为一个全局变量,被提升到了render函数外,在render函数内部返回这个vnode对象的引用。

image.png

这样做的好处是,在我们修改响应式数据,导致重新渲染而执行render函数时,不需要重复的创建这些静态节点(因为他们是不会改变的),省去了静态节点创建vnode的过程,优化了模板编译的性能。

patchFlag、Block优化diff算法

传统diff的缺点

传统的diff算法,会对vnode中所有的children分别进行判断,找出新旧vnode的共同之处,尽可能的复用结点而避免创建dom造成的性能开销。

但是这有一个坏处就是,传统的diff算法会对静态节点也进行处理,但是这些静态节点是不会变化的,对静态节点进行diff比较是没有意义的

vue3的改进:patchFlag

在Vue2中是无法在diff算法执行过程中过滤静态节点的,因为vnode中保存的信息太少,无法判断一个结点是静态的还是动态的。所以Vue3在进行diff优化时,在vnode上添加了额外的信息,使得在运行时能区分这个vnode是动态的还是静态的。

假如我们有下面这个模板

<div>
  <div>jzsp</div>
  <p>{{ message }}</p>  
</div>

传统的虚拟DOM会这样描述上面的模板:

const vnode = {
    tag:'div',
    children:[
        { tag:'div',children:'jzsp' },
        { tag:'p',children:ctx.message }
    ]
}

传统的虚拟DOm中没有任何的标志能识别出某个结点是静态的还是动态的,所以在Vue3优化之后,新的虚拟DOM会这样描述上面的模板:

const vnode = {
    tag:'div',
    children:[
        { tag:'div',children:'jzsp' },
        { tag:'p',children:ctx.message , patchFlag:1 }  //这是动态结点
    ]
}

也就是说,如果是动态结点的话,对应的vnode中会存在patchFlag属性。

vue3的改进:Block

既然我们已经可以判断vnode是否是动态结点了,那么我们就可以在虚拟结点创建的时候,将它子代的动态虚拟结点提取出来,并且保存在该虚拟节点的dynamicChildren数组内

const vnode = {
    tag:'div',
    children:[
        { tag:'div',children:'jzsp' },
        { tag:'p',children:ctx.message , patchFlag:1 }  //这是动态结点
    ],
    dynamicChildren:[
      // p标签具有patchFlag属性,因此它是动态结点。
      { tag:'p',children:ctx.message , patchFlag:1 }
    ]
}

观察上面的vnode对象可以发现,与普通的虚拟结点比,它多出了一个额外的属性:dynamicChildren。我们把带有这个属性的虚拟节点称之为“块”,也就是Block。

渲染器在更新一个Block时,会直接取出dynamicChildren进行更新,进而忽略掉那些静态节点。

注意:并不是所有的vnode都是Block,当我们在编写模板代码的时候,模板的根节点会是一个Block结点

<template>
<!-- 这个div是Block -->
  <div>  
    <p>jzsp</p>
  </div>
</template>

Block收集动态结点

我们得先知道一点:在render函数内,对createVNode函数的调用是层层嵌套的结构,并且函数的执行顺序是从内而外的(因为如果要执行最外层的函数的话,必须得先执行完第二层的函数并获取返回值,以此类推)。如下:

// 最内层的createVnode先执行完成。
createBlock('div',{},[
    createVnode('div',{},[
        createVnode('div',{},[
            createVnode('div',{},[

            ])
        ])
    ])
])

让我们来重新看一下前面贴出来的转化后的render函数。可以看到在return的时候,先后调用了openBlockcreateBlock方法,并且createBlock内部也是嵌套地调用了createVnode方法

image.png

先来介绍openBlock,它会创建一个数组(用于存储VNode)并且压入全局的blockStack(存储的是VNode数组)中,同时将这个创建的数组保存在全局的变量currentBlock中。

image.png

接着让我们看一下createVnode函数跟BLock有关的核心实现。在createVode最后会判断currentBlock是否存在,以及判断patchFlag(判断是否是动态结点),如果是动态结点并且currentBlock存在的话,就将当前的vnode压入全局的currentBlock中。具体如下:

image.png

再让我们看一下最后执行的createBlock函数,它做的事情很简单,就是将全局的currentBLock赋值到当前vnode对象的dynamicChildren属性中(此时的currentBlock已经包含了子代的所有动态结点,因为嵌套层级时内部先执行),如下。

image.png

那么在我们更新组件的时候,会优先判断新旧vnode中是否存在dynamicChildren属性。如果有的话,就只对这些dynamicChildren进行diff算法的比较。这样就可以省去大量的静态节点的比较,提高了vue的性能。

image.png