爲了提升小程序的開發效率,咱們團隊開發了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%。