基于 Vue 的小程序开发框架性能优化实践---去除 VNode

为了提升小程序的开发效率,咱们团队开发了Mars 框架,可使用 Vue 语法开发小程序,同时支持编译到 H5。近期咱们进行了 Mars 框架的性能升级(0.3.x 版本),极大简化了 Vue 的 render 过程,去掉了 VNode 构建,省略了 patch 过程,从而得到了性能提高。javascript

Mars 框架原理简介,为何要去除 VNode?

为了方便你们理解,这里简单说一下 Mars 框架的原理,目前基于 Vue 的小程序开发框架原理差别不大。html

详细的原理你们能够看这篇文章:Mars - 又双叒叕一个多端开发框架?此次是 Vue 驱动,能完美适配 H5vue

Mars 的原理以下图所示:java

Mars 原理图

上图中,左半部分表示小程序的执行部分。粉红色区域表明小程序视图,蓝色部分表明小程序的逻辑执行部分,视图与逻辑之间交换的是数据和事件。右边绿色部分是咱们在小程序逻辑以外,单首创建的 Vue 实例。小程序逻辑(蓝色部分)与 Vue 实例(绿色部分)是以以下方式工做的:node

  • 在小程序的 Page 建立时,咱们会同步 new 一个 Vue 实例。
  • 在 Vue 实例的 .$mp.scope 变量中绑定小程序实例,小程序实例中也会使用 .$vue 变量来绑定 Vue 实例,用于后续的数据传递。
  • 使用 handleProxy 方法代理小程序中的事件,当小程序事件发生时,对应执行 Vue 实例中相应的 Method。
  • 页面中的逻辑执行在 Vue 部分,每当 Vue 的视图更新时,在 Updated 阶段将数据的变化使用 setData 方法同步给小程序实例,触发小程序视图的刷新。

能够看到优化前咱们基本保留了 Vue 的全部渲染过程,只是删除了 Vue 中的 DOM 操做部分。因为 Vue 实例与小程序之间交换的只有数据,所以 Vue 中的视图层实际上是没有用到的。 咱们须要的只是执行 Vue 中的逻辑,判断数据修改是否会形成视图更新,视图更新时把变化的数据同步给小程序。而 Vue 视图层相关的内容,VNode、render、patch 这些不少是没有必要的,咱们的想法是经过精简没必要要的操做来提高性能。git

优化前 render 和 patch 过程所起的做用

想要精简 render 和 patch,咱们就须要先搞清楚 render 和 patch 在 Vue 中起到了什么做用:github

  1. 在 Vue 中,当数据发生变化时,会通知视图渲染依赖这一数据的全部实例,依次执行这些实例的 render 函数,此次 render 函数执行过程当中又会从新收集依赖,用于下一次数据发生变化时的依赖追踪。
  2. render 函数执行后会返回一个该实例对应的 VNode 树,render 过程当中并不会建立子组件实例,仅仅是生成了一个占位符。这个 VNode 树随后会传递给 patch 过程。
  3. patch 过程会将当前 VNode 树与旧 VNode 树进行 diff,以后根据 diff 建立、销毁子组件实例,修改 DOM 完成渲染。

在小程序框架这个情境下,咱们须要的是 数据依赖追踪组件实例建立、销毁,其余部分的内容则能够进行删减。小程序

咱们能够精简哪些内容?

  • render 函数部分,咱们只须要进行必要的依赖追踪,不须要建立 VNode 节点。
  • patch 部分,因为没有 VNode 了,咱们也不须要进行耗时的 diff 操做了!

可是等一下,没有了 VNode 树,如何建立组件实例呢?咱们将子组件的 Vue 实例建立改到了小程序子组件的生命周期中,也就是说单个 Vue 实例只会建立它本身,不会在继续建立子组件实例。 以前的结构为小程序实例树和 Vue 实例树,组件实例间互相绑定。如今的结构变为只有小程序实例树,每一个小程序实例节点单独对应一个 Vue 实例。api

开始实践!

下面介绍一下咱们具体作了哪些内容。数组

createComponent 中建立 Vue 实例

因为把 patch 过程干掉了,所以咱们须要手动建立子组件的 Vue 实例,同 Page 同样,咱们在 Component 的生命周期函数中 new 一个 Vue 实例,并与当前小程序实例绑定:

this.$vue = new VueComponent(options);
this.$vue.$mp = {
    scope: this
};
复制代码

在组件中建立 Vue 实例时,以前 Vue 中的父子关系没有了,维护这一关系须要解决如下问题:父元素绑定properties 传递

父元素绑定

在 patch 过程当中,Vue 建立子组件时会传递如下三个参数:

const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
}
复制代码
  • _isComponent 用于优化 options 的合并,咱们能够直接设置成 true。
  • _parentVnode 用于在 render 过程当中获取父元素信息,例如 scope-slot 等,因为咱们已经把 VNode 删掉了,所以再也不须要了。
  • parent 用于获取根元素、绑定 $children 等操做,Vue 就是经过这个参数来维护实例间的父子关系的。

咱们须要找到当前 Vue 实例的父实例,做为 parent 参数,从而完成父元素绑定过程。 小程序当前没有机制来直接获取父元素,须要咱们本身想办法来查找。在以前开发 Mars 过程当中,为了进行小程序组件实例和 Vue 组件实例间的匹配,对小程序实例树和 Vue 实例树中的组件节点都进行了标记,如今不须要进行实例间匹配查找了,可是咱们能够经过这个标记来查找父元素。

  • 因为 Page 元素可能在同一时间不惟一(因为页面切换),所以每建立一个 Page 实例,都须要绑定一个惟一的 rootUID,咱们将其存储在了getApp().__pages__中。rootUID 会逐层传给每一个小程序自定义组件实例。
  • 每次有小程序自定义组件实例建立,咱们都将该实例以标记的 id 为 key 存储在 getApp().__pages__[rootUID].__vms__中。
  • 根据 rootUID 找到根元素,进而找到 page 中的 __vms__
  • 根据 compId 算出父实例的 compId。
  • 根据父实例的 compid从__vms__中找到父元素,做为 parent。

properties 传递

除了须要设置的初始化属性外,咱们还须要传递子组件的 properties,不然父元素的数据没办法传递给子组件。

  • 数据初始化:能够在 Vue 建立时传入 propsData 来做为 props 的初始数据。 因为小程序自定义组件的参数和 Vue 子组件实例的参数是相同的,所以咱们能够直接将程序自定义组件的参数做为propsData在 new Vue 时传入:

    const options = {
        mpType: 'component',
        mpInstance: this,
        propsData: properties,
        parent
    };
    
    // 初始化 vue 实例
    this.$vue = new VueComponent(options);
    复制代码
  • 数据更新:仿照 Vue 给子组件传参数的机制,每次 render 时,将 props 从新给子组件赋值一遍。

只须要更新第一层,由于 properties 若是是对象,那么它在父元素中已经作过变化追踪了。

事件传递

对于 template 上绑定的事件,因为咱们自己已经使用了 handleProxy 来处理,所以不会受到影响。

须要处理的是 .$emit.$on 方法。

  • 对于 .$emit,咱们利用小程序机制,使用 triggerEvent 在小程序层面给父元素传递事件。
  • 对于 .$on,使用 Vue 现成的机制就好,不须要作额外工做,不过这也形成 Vue 的事件机制不能删除。

这里有个小坑:triggerEvent 方法传递的参数,须要从 event.detail 中获取,Mars 兼容了这个 diff。

render 函数精简

render 函数目前咱们不能彻底删除,由于须要如下两个功能:依赖收集复杂表达式和filter 计算

依赖收集

Vue 在初始化时会对实例上的 data 进行响应式处理,设置 set 和 get 方法。组件执行 render 函数时,会读取变量触发 get 方法,从而在 get 方法中将当前实例收集为这个数据的依赖。下次数据更新时 Vue 会通知依赖进行更新。

为了收集依赖,咱们须要在 render 函数中读取一遍数据。这里咱们将 VNode 树编译为数组树的形式,只留下数据,剩下的内容均可以删除。

好比这样的一个 template:

<template>
    <view class="hello">
        <view @tap="tapHandler">
            <text>https://github.com/max-team/Mars</text>
        </view>
        <view>{{ aaa }}</view>
        <view>{{ ccc }}</view>
        <name :name="nameOutter"></name>
        <view>{{ aaaComp }}</view>
    </view>
</template>
复制代码

Vue 产出的 render 函数是这样的:

// 修改前的 render 函数
_c('view',{staticClass:"hello"},[_c('view',{on:{"tap":_vm.tapHandler}},[_c('text',[_vm._v("https://github.com/max-team/Mars")])]),_c('view',[_vm._v(_vm._s(_vm.aaa))]),_c('view',[_vm._v(_vm._s(_vm.ccc))]),_c('name',{attrs:{"name":_vm.nameOutter,"compId":(_vm.compId ? _vm.compId : '$root') + ',0'}}),_c('view',[_vm._v(_vm._s(_vm.aaaComp))])],1)
复制代码

精简后咱们获得的 render 函数是这样的:

// 修改后的 render 函数
[,[,,[(_vm.aaa)],,[(_vm.ccc)],,[[_vm.nameOutter,(_vm.compId ? _vm.compId : '$root') + ',0']],,[(_vm.aaaComp)]]]
复制代码

能够看到 Vue 中的大量 render helper 掉用,例如 _c_v_s 等均可以省略了。

有些 render helper 仍是不能去掉,例如 v-for 循环,咱们仍是保留了 _l 函数,由于 v-for 循环的对象可能为数组、字符串、数字等多种状况。

复杂表达式和filter 计算。

在 Vue 的 template 中,是能够像 js 同样执行不少计算的,好比能够执行定义好的 method:

<div :prop="someMethod(data)"></div>
复制代码

或者执行一个 filter

<div :prop="someMethod | someFilter"></div>
复制代码

这部分的计算以前是在 render 中随着 VNode 构建执行的,计算结果存储在了 VNode 节点中。如今咱们没有 VNode 了,计算出的值怎么办呢?

  • 计算复杂表达式和 filter 的过程还在 render 过程当中保留。
  • 计算出的值使用 _ff 方法包裹。每一个计算值产生一个惟一的 id,_ff 方法将这些值按照 id 存储下来 setData 给小程序,小程序直接使用这些计算结果来进行渲染。

patch 过程

patch 过程已经彻底不须要了,咱们将这一过程彻底删除。

顺带解决的一个坑

在以前的方案中,从 Page 开始建立的小程序组件实例树,与 Vue 组件实例树是相互独立的。为了让小程序组件实例与 Vue 组件实例之间可以对应上(不然没法在组件级别 setData),咱们须要对每一个组件实例进行标记,经过标记来寻找对应关系。这在一些特殊情景下是会有问题的,例如组件快速生成又销毁等,形成实例间不匹配。

修改后的方案因为 Vue 实例是以组件级别建立的了,所以再也不会出现实例没法匹配的状况。

结果和总结

咱们使用了线上业务进行验证,渲染时间 -16%。此外,因为咱们精简了 Vue 的功能,删除了这部分功能的代码,框架总体的体积也减小了 11%。

相关文章
相关标签/搜索