從函式式元件引發的效能思考

Gerryli發表於2021-07-17

簡介

vue函式式元件大部分人在開發過程中用到的不多,就連官方文件位置放置的也比較隱晦,但是在我們對專案做效能優化時,卻是一個不錯的選擇。本文將對函式式元件初始化過程做一個系統性的闡述,通過本文,你將瞭解到以下內容:

  • 什麼是函式式元件
  • 函式式元件與普通元件間的差異
  • vue相似效能優化點

什麼是函式式元件

函式式元件即無狀態元件,沒有datacomputedwatch,也沒有生命週期方法,元件中也沒有this上下文,只有props傳參。在開發中,有很多元件僅僅只用到了props和插槽,這部分元件就可以提煉為函式式元件。借用官網demo,最簡單的函式式元件如下:

Vue.component('my-component', {
  functional: true,
  // Props 是可選的
  props: {
    // ...
  },
  // 為了彌補缺少的例項
  // 提供第二個引數作為上下文
  render: function (createElement, context) {
    // ...
  }
})

函式式元件與普通元件間的差異

元件例項化過程大致分為四步,狀態初始化 --> 模板編譯 --> 生成VNode --> 轉換為真實DOM。接下來對比普通元件與函式式元件常用配置項,比較下差異。

功能點名稱 普通元件 函式式元件 描述
vm Y N 元件作用域
hooks Y N 生命週期鉤子
data Y N 資料物件宣告
computed Y N 計算屬性
watch Y N 偵聽器
props Y Y 屬性
children Y Y VNode 子節點的陣列
slots Y Y 一個函式,返回了包含所有插槽的物件
scopedSlots Y Y 作用域插槽的物件
injections Y Y 依賴注入
listeners Y Y 事件監聽
parent Y Y 對父元件的引用

從上表中可以看出,普通元件與函式式元件最大的差別在於函式式元件沒有獨立作用域,沒有響應式資料宣告。沒有獨立作用域,會有以下優點:

  1. 沒有元件例項化(new vnode.componentOptions.Ctor(options)),函式式元件獲取VNode僅僅是普通函式呼叫

    • 無公共屬性、方法拷貝
    • 無生命週期鉤子呼叫
  2. 函式式元件直接掛載到父元件中,縮短首次渲染、diff更新路徑

    • 函式式元件在父元件生成VNode時,函式式元件render方法會被呼叫,生成VNode掛載到父元件children中,patch階段可直接轉換成真是DOM,普通元件則在createElm時,走元件初始化流程。
    • diff更新時,函式式元件呼叫render,直接建立普通VNode,而普通元件建立的VNode的是包含元件作用域的,diff操作時,還有額外呼叫updateChildComponent更新屬性、自定義事件等,呼叫鏈路會比較長。

vue效能優化點

函式式元件帶來的效能提升主要體現在縮短渲染路徑減少元件巢狀層級,前者與瀏覽器重繪迴流有異曲同工之處,後者可以降低時間複雜度。

無論何種效能優化,能從程式碼層面做優化的,無疑是代價最小的,雖然有時效果不是很明顯,但是積少成多。在vue中,有不少與上述相似的點,可以提升程式碼執行效率。

合理宣告data中資料,確保data中宣告資料都是必須的

很多時候有一些資料沒必要宣告在data中,比如需要元件內共享,但不需要響應式處理的資料。data中的資料,物件都會對其深度優先用Object.defineProperty宣告,陣列也會攔截基本操作方法。不必要的宣告會造成無意義的資料劫持

合理使用computedwatch

computedwatch最大的區別在於computed是惰性載入的。惰性載入主要體現在兩個方面:

  1. 依賴狀態發生改變時,不會立即觸發,只是改變當前Watcher例項的dirty屬性值為true
  2. 當對計算屬性值取操作時,當且僅當watcher.dirty === true時,才會觸發計算

以上兩點特性,能夠避免一些不必要的程式碼執行,具體程式碼如下所示:

// src\core\instance\state.js
function createComputedGetter (key) {
  return function computedGetter () {
    // 獲取例項上的computed屬性的watcher例項
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 當且僅當computed依賴屬性發生變化 && 對計算屬性進行取操作,才會呼叫Watcher的update方法,將dirty置為true
      if (watcher.dirty) {
        // 呼叫get方法,獲取到computed的值
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
// src\core\observer\watcher.js
update () {
  // computed watcher lazy === true
  if (this.lazy) {
    // 標記懶執行是否可執行狀態,false不執行計算屬性計算
    this.dirty = true
  } else if (this.sync) { // 同步執行
    this.run()
  } else {
    // 將當前watcher放入到watcher佇列
    queueWatcher(this)
  }
}

v-for繫結key值

v-for迴圈定義key值目的是便於精準找到diff比對節點,避免一些無意義的比對。

普通diff: 從頭尾開始,新舊節點頭尾分別比較,遊標向中間靠攏,當且僅當一個節點遍歷結束後,diff流程結束

帶有key值diff: 根據key值維護一個hash表,每次迴圈精準定位到更新目標節點,當且僅當一個節點遍歷結束後,diff流程結束

思考

vue中,很多效能優化點都是縮短程式碼執行路徑,尤其在存在大量計算邏輯中,效能的提升會有肉眼可見的效果。實際開發中,也有不少場景可以用到此類優化方法,舉個最簡單的例子,關鍵詞高亮匹配。實現這個操作,需要以下幾步:

  1. 獲取匹配關鍵詞,將關鍵詞進行格式化(對正規表示式中有意義的字串進行轉義)
  2. 動態生成匹配的正規表示式
  3. 根據正規表示式進行replace操作

有些時候,第一, 二步我們可以省略,直接執行第三步即可,因為輸入關鍵字可能存在相同的,因此我們可以將字串與正規表示式快取在Map中,下次匹配時,如果存在快取,直接從快取中拿即可。

vue模板編譯用到的就是這個特性,每次會把編譯的模板字串作為key值,render方法作為value,快取起來,如果遇到一樣的模板,可以省去編譯流程,帶來一定的效能提升。

小結

養成良好的編碼習慣,對於個人能力,也是一個不錯的提升。

相關文章