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

曉前端發表於2020-12-14

作者:秦志英

前言

上一篇文章中我們分析了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簡書開源中國部落格園 關注我們。

相關文章