【原始碼系列#03】Vue3計算屬性原理(Computed)

柏成發表於2023-12-07

專欄分享:vue2原始碼專欄vue3原始碼專欄vue router原始碼專欄玩具專案專欄,硬核💪推薦🙌

歡迎各位ITer關注點贊收藏🌸🌸🌸

語法

傳入一個 getter 函式,返回一個預設不可手動修改的 ref 物件

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 錯誤!

或者傳入一個擁有 get 和 set 函式的物件,建立一個可手動修改的計算狀態

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  },
})

plusOne.value = 1
console.log(count.value) // 0

原始碼實現

  • @issue1 computed引數相容只傳getter方法和handler物件的情況

  • @issue2 快取特性,只要依賴的變數值沒有發生變化,就取快取中的值

    _dirty作為快取標識,如果依賴的變數值有變化,則將 _dirty 值置為 true,後續讀取計算屬性時,重新執行getter;否則直接取_value

  • @issue3 巢狀effect,firstname -> 計算屬性fullName -> effect,下一章節詳細介紹

import { isFunction } from '@vue/shared'
import { ReactiveEffect, trackEffects, triggerEffects } from './effect'

/**
 * @issue1 computed引數相容只傳getter方法和handler物件
 * @issue2 快取,只要依賴的變數值沒有發生變化,就取快取中的值
 * @issue3 巢狀effect,firname -> fullName -> effect
 */
class ComputedRefImpl {
  public effect
  public _dirty = true // 預設應該取值的時候進行計算
  public _value
  public dep = new Set()
  public __v_isReadonly = true
  public __v_isRef = true
  constructor(public getter, public setter) {
    // 我們將使用者的getter放到effect中,這裡面firstname和lastname就會被這個effect收集起來
    this.effect = new ReactiveEffect(getter, () => {
      // 稍後依賴的屬性firstname、lastname變化了,會執行此排程函式
      if (!this._dirty) {
        this._dirty = true
        // 實現一個觸發更新 @issue3
        triggerEffects(this.dep)
      }
    })
  }
  
  // 類中的訪問器屬性 底層就是Object.defineProperty
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/get
  get value() {
    // 做依賴收集 @issue3
    trackEffects(this.dep)
    // @issue2
    if (this._dirty) {
      // 說明這個值是髒的
      this._dirty = false
      this._value = this.effect.run()
    }
    return this._value
  }
  
  set value(newValue) {
    this.setter(newValue)
  }
}

export const computed = getterOrOptions => {
  let onlyGetter = isFunction(getterOrOptions)

  let getter
  let setter
  // @issue1 
  if (onlyGetter) {
    getter = getterOrOptions
    setter = () => {
      console.warn('no set')
    }
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(getter, setter)
}

trackEffects 和 triggerEffects 方法如下

export function trackEffects(dep) { // 收集dep 對應的effect
  if (activeEffect) {
    let shouldTrack = !dep.has(activeEffect) // 去重了
    if (shouldTrack) {
      dep.add(activeEffect)
      // 存放的是屬性對應的set
      activeEffect.deps.push(dep) // 讓effect記錄住對應的dep, 稍後清理的時候會用到
    }
  }
}

export function triggerEffects(effects) { 
  effects = new Set(effects);
  for (const effect of effects) {
    if (effect !== activeEffect) { // 如果effect不是當前正在執行的effect
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run(); // 重新執行一遍
      }
    }
  }
}

巢狀 effect

讓我們分析一下這個測試用例

const { effect, reactive, computed } = VueReactivity
const state = reactive({ firname: '李', lastname: '柏成' })

const fullName = computed(() => {
  // defineProperty中的getter
  return state.firstname + state.lastname
})

effect(() => {
  app.innerHTML = fullName.value
})

setTimeout(() => {
  state.firstname = '王'
}, 1000)

// 1. firstname要依賴於計算屬性的effect
// 2. 計算屬性收集了外層effect
// 3. 依賴的值變化了會觸發計算屬性effect重新執行, 計算屬性重新執行的時候會觸發外層effect來執行

// computed 特點:快取
console.log('fullName.value', fullName.value)
console.log('fullName.value', fullName.value)
  1. 當執行到 renderEffect 時,預設先執行一次 effect.run(),activeEffect --> renderEffect,並執行 this.fn() --> app.innerHTML = fullName.value
effect(() => {
  app.innerHTML = fullName.value
})
  1. 當訪問 fullName.value 時,在 getter 方法中執行 trackEffects(this.dep),計算屬性fullName 依賴收集 當前的 activeEffect(renderEffect)
  2. 當執行 this._value = this.effect.run() 時,activeEffect --> computedEffect,並執行 this.fn() ---> return state.firstname + state.lastname
  3. 訪問了state.firstname,屬性 firstname 依賴收集當前的 activeEffect(computedEffect)
  4. 訪問了state.lastname,屬性 lastname 依賴收集當前的 activeEffect(computedEffect)
  5. 一秒鐘後,firstname 發生了變化。。。firstname變化觸發更新 triggerEffects --> computedEffect.scheduler()
  6. 在計算屬性 scheduler 中,觸發更新 triggerEffects(this.dep) --> renderEffect.run() ,最終重新渲染頁面 app.innerHTML = fullName.value

相關文章