Vue3 中性能的提升

218 阅读9分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

Vue3 中性能提升可以从下面几个方面介绍:

  • Vue3 使用 props对象重写的响应式系统。

  • 其次通过优化编译的过程和重写虚拟 DOM 提升渲染的性能。

  • 通过优化源码的体积和更好的 TreeShaking 的支持,减少打包的体积。

响应式系统升级

我们都知道 Vue2 的时候,数据响应式的原理使用的是 defineProperty,在初始化的时候会遍历 data 中的所有成员,通过 defineProperty 把对象的属性转换成 get 和 set。如果 data 中的属性又是对象的话,需要递归处理每一个子对象的属性,注意这些都是在初始化的时候进行的。也就是如果你没有使用这个属性的时候,也把它进行了响应式的处理。

Vue3 中采用的是 ES6 以后新增的 props 对象,props 对象的性能本身就比 defineProperty 要好。另外代理对象可以拦截属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有的属性。另外,如果有多层属性嵌套的话,只有访问某个属性的时候才会递归处理下一级的属性。使用 propss 对象默认就可以监听到动态添加的属性。而 Vue2 里边想要动态添加一个响应式的属性,需要调用 vue.set 的方法来处理。而且 Vue2 中还监听不到属性的删除,对数组的索引和 length 属性的修改也监听不到,Vue3 中使用代理对象可以监听属性的删除以及数组的索引和 length 属性的修改操作。所以 Vue3 中使用process 对象提升了显示系统的性能和功能。

编译优化

除了显示系统的升级以外,Vue3 中通过优化编译的过程和重写虚拟的 DOM,让首次渲染和更新的性能有了大幅度的提升。

那下面我们通过一个组件,先来回顾一下 Vue2 中编译的执行过程。我们知道 Vue 的时候,模板首先需要编译成 render 函数,这个过程一般是在构建的过程中完成的。在编译的时候会编译静态根节点和静态节点。静态根节点要求节点中必须有一个静态子节点。例如这个位置,当组件的状态发生变化后,会通知 Watcher,触发 Watcher update,最终去执行虚拟 DOM 的 patch 操作,遍历所有的虚拟节点找到差异,然后更新到真实的 DOM 上。diff 的过程中会去比较整个虚拟 DOM,先对比新旧的 DIV 以及它的属性,然后再对比它内部的子节点。那 Vue2 中它渲染的最小单位组件。

Vue2 中 diff 的过程会跳过静态根节点,因为静态根节点的内容不会发生变化。也就是 Vue2 中通过标记静态根据点优化了的过程。但是在 Vue2 的时候,静态节点还需要再进行 diff,这个过程没有被优化。

Vue3 中为了提高性能,在编译的时候会标记和提升所有的静态节点,然后 diff 时候只需要对比动态节点的内容。另外在 Vue3 中新引入了一个 Fragments,也就是片段的特性,模板中不需要再创建一个唯一的根节点模板里边可以直接放文本内容或者很多同级的标签,在 vs code 中需要升级你的插件。否则,模板中如果没有唯一的根结点,vs code 依然会提示有错误。

那下面我们来看一下v 五三中模板编译的结果。我们来打开v 五三的time plate 二。 左边是我们刚刚看到的组件模板中的内容。右边是我们编译之后的render 函数,

但是这个编译的结果跟 Vue2 会有很大的区别。首先这里调用 createBlock 给我们的根 div 创建了一个block,它是一个树的结构。然后通过 createVNode 去创建了我们的子节点。那这个的 createVNode 其实就类似于我们之前的 H 函数。那我们来删除这里面的根节点来看一下它的变化。

image.png

当我们删除根基点之后,这里会创建一个 Fragments。也就是我们之前说的片段,其实从这里还可以看到,它内部还是维护了一个树形的结构。那我们最外层是 Fragments,里面是我们的这些 VNode。

那我们看右上角有一个 Options,options 里面有一个 hoistStatic 提升静态内容。我们选上之后。

image.png

此时代码跟刚刚发生了一个变化,我们 createBlock 里面的以前的那些 createVNode 的,我们创建的那些静态节点都被提升到了 render 函数的外面。那我们可以看到这里的 createVNode,所以它对应的是我们的静态节点。我们的动态节点,比如我们下面的这个 div,它绑定了我们下面这个按钮,它绑定了一个事件。那他们两个并没有提升,我们提升的是静态节点,也就是标签里面的内容是纯文本的内容。那这些静态节点都会被提升到 render 的那外层来。那这些被提升的静态节点,只有在初始化的时候会被创建一次。当我们再次进入 render 函数的时候,不需要再次创建这些静态节点。因为之前曾经创建过,我们可以直接重用上一次创建的这些静态节点对应的 VNode 的。

那我们再来看一下模板中的这个 Div ,它里面绑定了一个差值表达式 count,还绑定了一个属性。那这个属性我们先删掉,我们一会儿再给他加看,加上属性之后有什么区别。这是一个动态的节点,它的内容会发生变化。我们来看一下对应的 render 中的代码。

image.png

注意这个位置,createVNode 的,然后他们创建的是一个 div ,那这里边会取得我们数据中的 count,然后把它展示在我们对应的位置处。那注意最后有一个数字 1,这是 patch flag,就是一个标记。那将来在执行 diff 的时候,会检查整个 block 里边带 patch flag 的标记的节点。那将来这些静态节点都会被直接跳过。而且在比较动态节点的时候,我们发现 patch flag 的值是1,1代表文本内容是动态绑定的。我们可以看到后边有个注释是 text,于是只会比较文本内容是否发生变化。那么再来给这个 div 动态绑定一个属性。

image.png

我们再来看 render 中的代码。注意我们刚刚看到 patch flag 的值变成了 9,我们来看一下后面的注释。那这块 9 代表的是当前的文本和 props 是动态内容。那再往后看,这里还记录了动态绑定的属性名称是 id 那将来 diff的时候,只会检查这个动态节点的文本和 id 属性是否发生变化。那这样大大提高了虚拟 DOm diff 的性能。

虚拟 DOM 中 diff 的过程是最耗时的,在 Vue2 中重新渲染的时候,需要重新去创建新旧 VNode,diff 的时候会跳过静态根节点对比剩下的每一个新旧 VNode,哪怕这个节点什么都没做。

Vue3 中通过标记和提升静态节点以及 patch flag 标记动态节点来大大提升了的性能。

那我们再来看 Option。这里还有一个 catchHandlers,也就是缓存事件处理函数。我们先来看一下不缓存的情况。

image.png

我们来看这个按钮,那我们再找到这个按钮对应的中的代码。这里通过 onclick 给这个按钮注册了一个事件。那 handler 就是我们的事件处理函数。那么呢 onclick 其实也可以看成一个属性。那提供的这个事件处理函数可能是由 data 中返回的。那之后有可能会把这个函数重新复制成另一个函数。那这个时候需要进行一次更新的操作。当数据变化之后会重新渲染视图,

那我们开启缓存。再来看一下。

image.png

当开启缓存之后,这块的代码发生了变化。当我们首次渲染的时候,它首先会去生成一个新的函数。那这个函数里面返回的就是我们的这个 handler 了,并且会把这个新的函数缓存到 catch 对象里面来,将来再次调用 reader 的时候,会直接从缓存中来获取我们上一次生成的这个函数。那注意我们缓存的这个函数,它永远不会发生变化,也就是绑定不会发生变化。但是你运行这个函数的时候,它会重新去获取最新的handler,那避免了不必要的更新。

那我们再来看我们在这个组件里边,假设我们用到了 transition,那我们增加一个 transition。

image.png

当我们加上这个 transition 标签之后,我们再来看在 import 里面它导入了 transition,也就是按需引用,我们不需要的时候是不会去打包这个 transition 的。

总结一下 Vue3 中在编译的过程中会通过标记和提升静态节点,然后通过 patch flag 将来在 diff 的时候会跳过静态根节点,只需要去更新动态节点中的内容。那大大提升了地图的性能。另外还通过对事件处理函数的缓存,减少了不必要的更新操作。

源码体积的优化

Vue3 中移出了一些不常用的 API,比如 inline-template、filter 等,可以让最终代码的体积变小。移除 filter 可以通过 method 或者计算属性来实现。

Vue3 对 tree-shaking 的支持更好,tree-shaking 依赖 ES Module ,也就是 ES 6的模块化语法的静态结构 import 和export,通过阶段的静态分析找到没有引入的模块,在打包的时候直接过滤掉,那让打包后的体积更小。

Vue3 在设计之初就考虑到了 tree-tracking 内置的组件,比如 transition、keep-alive 和一些内置的指令。比如 v-module 都是按需引入的。

Vue3 中的很多 API 都是支持 tree-traking 的。所以 Vue3 中的一些新增的 API,如果你没有使用的话,这部分代码是不会打包的,只会打包你所使用的 API。但是默认 Vue 的核心模块都会被打包。

Vue3 三在设计的时候就考虑到了性能的问题,通过代码的优化,编译的优化或者打包来提升性能。