Vue3源碼解析(computed-計算屬性)

作者:秦志英

前言

上一篇文章中我們分析了Vue3響應式的整個流程,本篇文章我們將分析Vue3中的computed計算屬性是如何實現的。

在Vue2中我們已經對計算屬性了解的很清楚了,在Vue3中提供了一個computed的函數作為計算屬性的API,下面我們來通過源碼的角度去分析計算屬性的運行流程。

computed

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}
  • 在最開始使用函數重載的方式允許computed函數接受兩種類型的參數:第一種是一個getter函數, 第二種是一個帶getset的對象。
  • 接下就是在函數內部根據傳入的不同類型的參數初始化函數內部的gettersetter函數,如果傳入的是一個函數類型的參數,那麼getter就是這個函數,setter就是一個空的操作,如果傳入的參數是一個對象,則getter就等於這個對象的get函數,setter就等於這個對象的set函數。
  • 在函數的結尾返回了一個new ComputedRefImpl,並將前面我們標準化後的參數傳遞給了這個構造函數。
    下面我們就來分析一下ComputedRefImpl這個構造函數。

ComputedRefImpl

class ComputedRefImpl<T> {
  // 快取結果
  private _value!: T
  // 重新計算開關
  private _dirty = true
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    // 對傳入的getter函數進行包裝
    this.effect = effect(getter, {
      lazy: true,
      // 調度執行
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          // 派發通知
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })
  }
  // 訪問計算屬性的時候 默認調用此時的get函數
  get value() {
    // 是否需要重新計算
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    // 訪問的時候進行依賴收集 此時收集的是訪問這個計算屬性的副作用函數
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

ComputedRefImpl類在內部維護了_value_dirty這兩個非常重要的私有屬性,其中_value使用用來快取我們計算的結果,_dirty是用來控制是否需要重現計算。接下來我們來看一下這個函數的內部運行機制。

  • 首先構造函數在初始化的時候使用了effect函數對傳入getter進行了一層包裝(上一篇文章中我們分析過effect函數的作用就是將傳入的函數變成可響應式的副作用函數),但是這裡我們在effect中傳入了一些配置參數,還記得前面我們分析trigger函數的時候有這一段程式碼:
const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
effects.forEach(run)

當屬性值發生改變之後,會觸發trigger函數進行派發更新,將所有依賴這個屬性的effect函數循環遍歷,使用run函數執行effect,如果effect的參數中配置了scheduler,則就執行scheduler函數,而不是執行依賴的副作用函數。當計算屬性依賴的屬性發生變化的時候,回執行包裝getter函數的effect, 但是因為配置了scheduler函數,所以真正執行的是scheduler函數,在scheduler函數中並沒有執行計算屬性的getter函數求取新值,而是將_dirty設置為false,然後通知依賴計算屬性的副作用函數進行更新, 當依賴計算屬性的副作用函數收到通知的時候就會訪問計算屬性的get函數,此時會根據_dirty值來確定是否需要重新計算。

回到我們的這個構造函數中,只需要記得我們在構造函數初始化三個重要的點:第一:對傳入的getter函數使用effect函數進行包裝。第二:在使用effect包裝的過程中,我們會執行getter函數,此時getter函數執行過程中對於訪問到的屬性會將當前的這個計算屬性收集到對應的依賴集合中, 第三:傳入了配置參數lazyscheduler,這些配置參數在當前的這個計算屬性所訂閱的屬性發生改變的時候,用來控制計算屬性的調度時機。

  • 接著我們繼續分析get value,當我們訪問計算屬性的值時候實際上訪問的就是這個函數的返回值, 它會根據_dirty的值來判斷是否需要重新計算getter函數,_dirty為true需要重新執行effect函數,並將effect的值置為false,否則就返回之前快取的_value值。在訪問計算屬性值的階段會調用track函數進行依賴收集,此時收集的是訪問計算屬性值的副作用函數, key始終是vlaue。
  • 最後就是當設置計算屬性的值的時候會執行set函數,然後調用我們傳入的_setter函數。

示例流程

至此計算屬性的執行流程就分析完畢了,我們來結合一個示例來完整的過一遍整個流程:

<template>
    <div>
        <button @click="addNum">add</button>
        <p>計算屬性:{{computedData}}</p>
    </div>
</template>

<script>
import { ref, watch,reactive, computed } from 'vue' 
import { effect } from '@vue/reactivity'
export default {
  name: 'App',
  setup(){
    const testData = ref(1)
    const computedData = computed(() => {
      return testData.value++
    })
    function addNum(){
      testData.value += 10
    }
    return {
      addNum,
      computedData
    }
  },
}

</script>

下面是一張流程圖,當點擊頁面中的按鈕改變testData的value值時,發生的變化流程就是下面的紅線部分。

  • 首先初始化頁面的時候,testData經過ref()之後變成響應式數據,會對訪問testData.value的值進行依賴收集,當testData.value的值發生變化的話,會對依賴這個值的依賴集合進行派發更新
  • computed中傳入了一個getter函數,getter函數內部有對testData.value的訪問,此時當前的這個計算屬性的副作用函數就訂閱了testData.value的值,computed返回了一個值,而頁面中的組件有對computed返回值的訪問,頁面的渲染副作用函數就訂閱了computed的返回值,所以這個頁面中有兩個依賴集合。
  • 當我們點擊頁面中的按鈕,會改變testData.value的值,此時會通知訂閱計算屬性的副作用函數進行更新操作,由於我們在生成計算屬性副作用的時候配置了scheduler,所以執行的是scheduler函數,scheduler函數並沒有立即執行getter函數進行重新計算,而是將ComputedRefImpl類內部的私有變數_dirty設置為true,然後通知訂閱當前計算屬性的副作用函數進行更新操作。
  • 組件中的渲染副作用函數執行更新操作的時候會訪問到get value函數,函數內部會根據_dirty值來判斷是否需要重新計算,由於前面的scheduler函數將_dirty設置為true所以此時會調用getter函數的副作用函數effect,這個時候才會重新計算並將結果返回,頁面數據更新。

總結

計算屬性兩個最大的特點就是

  • 延時計算 計算屬性所依賴的值發生改變的時候並不會立即執行getter函數去重新計算新的結果,而是打開重新計算的開關並通知訂閱計算屬性的副作用函數進行更新。如果當前的計算屬性沒有依賴集合就不執行重新計算邏輯,如果有依賴觸發計算屬性的get,這個時候才會調用this.effect()進行重新計算。
  • 快取結果 當依賴的屬性沒有發生改變的,訪問計算屬性會返回之前快取在_value中的值。

對 Electron 感興趣?請關注我們的開源項目 Electron Playground,帶你極速上手 Electron。

我們每周五會精選一些有意思的文章和消息和大家分享,來掘金關注我們的 曉前端周刊


我們是好未來 · 曉黑板前端技術團隊。
我們會經常與大家分享最新最酷的行業技術知識。
歡迎來 知乎掘金SegmentfaultCSDN簡書開源中國部落格園 關注我們。

Tags: