天天看點

antd vue 樹更新資料後不展開_Vue 性能優化 - 去除 VNode

作者:梅旭光 @Mayo

為提高小程式的開發效率,百度APP移動團隊開發了Mars 架構。該架構支援使用 Vue 文法開發小程式,同時支援生成對應的 H5 頁面。在 Mars 架構的

0.3.x 版本

中,我們極大簡化了 Vue 的 render 過程,去掉了 VNode 建構,省略了 patch 過程,進而獲得了性能提升。

Mars 架構原理簡介,為什麼要去除 VNode?

目前基于 Vue 的小程式開發架構原理差異不大。為友善大家了解,這裡簡單說一下 Mars 架構的原理。

Mars 的原理如下圖所示:

antd vue 樹更新資料後不展開_Vue 性能優化 - 去除 VNode

上圖中,左半部分表示小程式的執行部分。粉紅色區域代表小程式視圖,藍色部分代表小程式的邏輯執行,視圖與邏輯之間交換的是資料和事件。右邊綠色部分則是在小程式邏輯之外,單獨建立的 Vue 執行個體。小程式邏輯(藍色部分)與 Vue 執行個體(綠色部分)是以如下方式工作的:

  • 在小程式的 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 這些操作在這種場景下是沒有必要的,可以通過精簡這些不必要的操作來提升性能。

優化前 render 和 patch 過程所起的作用

想要精簡 render 和 patch 過程,我們就需要先搞清楚 render 和 patch 過程在 Vue 中起到了什麼作用:

  1. 在 Vue 中,當資料發生變化時,會通知視圖渲染依賴這一資料的所有執行個體,依次執行這些執行個體的 render 函數。render 函數執行過程中又會重新收集依賴,用于下一次資料發生變化時的依賴追蹤。
  2. render 函數執行後會傳回一個該執行個體對應的 VNode 樹, render 過程中并不會建立子元件執行個體 ,僅僅是生成了一個占位符。這個 VNode 樹随後會傳遞給 patch 過程。
  3. patch 過程會将目前 VNode 樹與舊 VNode 樹進行 diff,之後根據 diff 建立、銷毀子元件執行個體,修改 DOM 完成渲染。

在小程式架構這個情境下,需要的是

資料依賴追蹤

元件執行個體建立、銷毀

,其他部分的内容則可以進行删減。

可以精簡哪些内容?

  • render 函數部分 ,我們隻需要進行必要的依賴追蹤,不需要建立 VNode 節點。
  • patch 部分 ,由于沒有 VNode 了,我們也不需要進行耗時的 diff 操作了!

但是等一下,剛才我們說過子元件執行個體是在 diff VNode 樹的過程中建立的,現在沒有 VNode 樹了,子元件執行個體如何建立呢?

解決方法是:

在小程式子元件的生命周期中建立對應的 Vue 執行個體

。也就是說,單個 Vue 執行個體隻會建立它自己,不會再繼續建立子元件執行個體。 之前的結構為小程式執行個體樹和 Vue 執行個體樹,元件執行個體間互相綁定。現在的結構變為隻有小程式執行個體樹,每個小程式執行個體節點單獨對應一個 Vue 執行個體。

開始實踐!

下面介紹去除 VNode 所做的具體工作。

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 相容了這個不一緻。

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>
           

Vue 中這部分的計算是在 render 中随着 VNode 的建構執行的,計算結果存儲在了 VNode 節點中。現在我們沒有 VNode 了,計算出的值怎麼辦呢?

  • 計算複雜表達式和 filter 的過程還在 render 過程中保留。
  • 計算出的值使用

    _ff

    方法包裹。每個計算值産生一個唯一的 id,

    _ff

    方法将這些值按照 id 存儲下來 setData 給小程式,小程式直接使用這些計算結果來進行渲染。

patch 過程

patch 過程已經完全不需要了,我們将這一過程完全删除。

順帶解決的一個坑

在之前的方案中,從 Page 開始建立的小程式元件執行個體樹,與 Vue 元件執行個體樹互相獨立。為了讓小程式元件執行個體與 Vue 元件執行個體之間能夠對應(否則無法在元件層面 setData),我們需要對每個元件執行個體進行标記,通過标記來尋找對應關系。這在一些特殊情景下是會有問題的,例如元件快速生成又銷毀等,可能出現執行個體間不比對的問題。

修改後的方案 Vue 執行個體以元件級别建立,是以不再會出現執行個體無法比對的情況。

結果和總結

去 VNode 優化後,使用線上業務驗證,渲染時間減少了 16%。此外,由于精簡了 Vue 的部分功能,架構整體的體積也減少了 11%。

繼續閱讀