基於 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%。

相關文章
相關標籤/搜索