为了提升小程序的开发效率,咱们团队开发了Mars 框架,可使用 Vue 语法开发小程序,同时支持编译到 H5。近期咱们进行了 Mars 框架的性能升级(0.3.x 版本),极大简化了 Vue 的 render 过程,去掉了 VNode 构建,省略了 patch 过程,从而得到了性能提高。javascript
为了方便你们理解,这里简单说一下 Mars 框架的原理,目前基于 Vue 的小程序开发框架原理差别不大。html
详细的原理你们能够看这篇文章:Mars - 又双叒叕一个多端开发框架?此次是 Vue 驱动,能完美适配 H5vue
Mars 的原理以下图所示:java
上图中,左半部分表示小程序的执行部分。粉红色区域表明小程序视图,蓝色部分表明小程序的逻辑执行部分,视图与逻辑之间交换的是数据和事件。右边绿色部分是咱们在小程序逻辑以外,单首创建的 Vue 实例。小程序逻辑(蓝色部分)与 Vue 实例(绿色部分)是以以下方式工做的:node
.$mp.scope
变量中绑定小程序实例,小程序实例中也会使用 .$vue
变量来绑定 Vue 实例,用于后续的数据传递。handleProxy
方法代理小程序中的事件,当小程序事件发生时,对应执行 Vue 实例中相应的 Method。setData
方法同步给小程序实例,触发小程序视图的刷新。能够看到优化前咱们基本保留了 Vue 的全部渲染过程,只是删除了 Vue 中的 DOM 操做部分。因为 Vue 实例与小程序之间交换的只有数据,所以 Vue 中的视图层实际上是没有用到的。 咱们须要的只是执行 Vue 中的逻辑,判断数据修改是否会形成视图更新,视图更新时把变化的数据同步给小程序。而 Vue 视图层相关的内容,VNode、render、patch 这些不少是没有必要的,咱们的想法是经过精简没必要要的操做来提高性能。git
想要精简 render 和 patch,咱们就须要先搞清楚 render 和 patch 在 Vue 中起到了什么做用:github
在小程序框架这个情境下,咱们须要的是 数据依赖追踪 和 组件实例建立、销毁,其余部分的内容则能够进行删减。小程序
可是等一下,没有了 VNode 树,如何建立组件实例呢?咱们将子组件的 Vue 实例建立改到了小程序子组件的生命周期中,也就是说单个 Vue 实例只会建立它本身,不会在继续建立子组件实例。 以前的结构为小程序实例树和 Vue 实例树,组件实例间互相绑定。如今的结构变为只有小程序实例树,每一个小程序实例节点单独对应一个 Vue 实例。api
下面介绍一下咱们具体作了哪些内容。数组
因为把 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 实例树中的组件节点都进行了标记,如今不须要进行实例间匹配查找了,可是咱们能够经过这个标记来查找父元素。
getApp().__pages__
中。rootUID 会逐层传给每一个小程序自定义组件实例。getApp().__pages__[rootUID].__vms__
中。__vms__
。__vms__
中找到父元素,做为 parent。除了须要设置的初始化属性外,咱们还须要传递子组件的 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 函数目前咱们不能彻底删除,由于须要如下两个功能:依赖收集、复杂表达式和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 循环的对象可能为数组、字符串、数字等多种状况。
在 Vue 的 template 中,是能够像 js 同样执行不少计算的,好比能够执行定义好的 method:
<div :prop="someMethod(data)"></div>
复制代码
或者执行一个 filter
<div :prop="someMethod | someFilter"></div>
复制代码
这部分的计算以前是在 render 中随着 VNode 构建执行的,计算结果存储在了 VNode 节点中。如今咱们没有 VNode 了,计算出的值怎么办呢?
_ff
方法包裹。每一个计算值产生一个惟一的 id,_ff
方法将这些值按照 id 存储下来 setData 给小程序,小程序直接使用这些计算结果来进行渲染。patch 过程已经彻底不须要了,咱们将这一过程彻底删除。
在以前的方案中,从 Page 开始建立的小程序组件实例树,与 Vue 组件实例树是相互独立的。为了让小程序组件实例与 Vue 组件实例之间可以对应上(不然没法在组件级别 setData),咱们须要对每一个组件实例进行标记,经过标记来寻找对应关系。这在一些特殊情景下是会有问题的,例如组件快速生成又销毁等,形成实例间不匹配。
修改后的方案因为 Vue 实例是以组件级别建立的了,所以再也不会出现实例没法匹配的状况。
咱们使用了线上业务进行验证,渲染时间 -16%。此外,因为咱们精简了 Vue 的功能,删除了这部分功能的代码,框架总体的体积也减小了 11%。