1. 程式人生 > 實用技巧 >淺談vue3中effect與computed的親密關係

淺談vue3中effect與computed的親密關係

在我剛看完vue3響應式的時候,心中就有一個不可磨滅的謎團,讓我茶不思飯不想,總想生病。那麼這個謎團是什麼呢?就是在響應式中一直穿行在tranger跟track之間的effect。如果單純的響應式原理根本就用不上effect,那麼effect到底是幹什麼的呢?

船到橋頭自然直,柳岸花明又一村。苦心人天不負,偶然間我看到了effect測試程式碼用例!

it('should observe basic properties', () => {
 let dummy
 const counter = reactive({ num: 0 })
 effect(() => (dummy = counter.num))

 expect(dummy).toBe(0)
 counter.num = 7
 expect(dummy).toBe(7)
})

解釋一下,這段程式碼

  • 首先宣告dummy變數,然後在effect的回撥中把已響應的物件counter的num屬性賦值給dummy
  • 然後做斷言判斷 dummy是否等於 0
  • 將 counter.num 賦值 7 ,然後 dummy 也變成了 7 !

這,,,讓我想到了什麼??

這就是computed的嗎?

趕緊看下 computed 的測試用例!!

const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)

哈哈哈

阿哈哈哈哈

hhhhhhhhhhhhhhhhhhhh

忍不住想仰天長嘯!!

果然跟我猜想的一樣!!!我終於直到effect是個什麼鬼了,顧名思義effect是副作用的意思,也就是說它是響應式副產品,每次觸發了 get 時收集effect,每次set時在觸發這些effects。這樣就可以做一些響應式資料之外的一些事情了,比如計算屬性computed。

讓我們用effect實現一個computed 可能會更清晰一點

我就不寫一些亂七八糟的判斷了,讓大家能夠看的更加清楚

function computed (fn) {
 let value = undefined
 const runner = effect(fn, {
  // 如果lazy不置為true的話,每次建立effect的時候都會立即執行一次
  // 而我們要實現computed顯然是不需要的
  lazy: true
 })
 // 為什麼要使用物件的形式,是因為我們最後需要得到computed的值
 // 如果不用物件的 get 方法的話我們就需要手動再呼叫一次 computed() 
 return {
  get value() {
   return runner()
  }
 }
}

// 使用起來是這樣的

const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1

console.log(cValue.value) // 1

這也太簡單了吧,那麼重點來了,effect怎麼實現的呢?

彆著急,我們先捋一下邏輯

  1. 首先 如果 effect 回撥內有已響應的物件被觸發了 get 時,effect就應該被儲存起來
  2. 然後,我們需要一個儲存effect的地方,在effect函式呼叫的時候就應該把effect放進這個儲存空間,在vue中使用的是一個數組activeReactiveEffectStack = []
  3. 再後,每個target被觸發的時候,都可能有多個effect,所以每個target需要有一個對應的依賴收集器 deps,等到 set 時遍歷 deps 執行 effect()
  4. 然而,這個依賴收集器 deps 不能放在 target 本身上,這樣會使資料看起來不是很簡潔,還會存在多餘無用的資料,所以我們需要一個 map 集合來儲存 target 跟 deps 的關係, 在vue中這個儲存集合叫 targetMap 。

幾個概念

track 追蹤器,在 get 時呼叫該函式,將所有 get 的 target 跟 key 以及 effect 建立起對應關係

// 比如 const react = reactive({a: { b: 2 })
// react.a 時 target -> {a: { b: 2 } key -> a 
// targetMap 儲存了 target --> Map --> key --> Set --> dep --> effect 
// 當呼叫 react.a.b.c.d.e 時 depsMap
// {"a" => Set(1)} --> Set --> effect
// {"b" => Set(1)}
// {"c" => Set(1)}
// {"d" => Set(1)}
// {"e" => Set(1)}
export function track(target: any, key: string) {
 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
 if (effect) {
  let depsMap = targetMap.get(target);
  if (depsMap === void 0) {
   targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key!);
  if (!dep) {
   depsMap.set(key!, (dep = new Set()));
  }
  if (!dep.has(effect)) {
   dep.add(effect);
   effect.deps.push(dep);
  }
 }
}

trigger 觸發器,這個就比較好理解了,拿到target key下的對應的所有 effect,然後遍歷執行 effect()

export function trigger(target: any, key?: string | symbol) {
 const depsMap: any = targetMap.get(target);
 const effects: any = new Set()
 if (depsMap && depsMap.get(key)) {
  depsMap.get(key).forEach((dep: any) => {
   effects.add(dep)
  });
  effects.forEach((e: any) => e())
 }
}

effect 函式實現

// 暴露的 effect 函式
export function effect(
 fn: Function,
 options: any = EMPTY_OBJ
): any {
 if ((fn as any).isEffect) {
  fn = (fn as any).raw
 }
 const effect = createReactiveEffect(fn, options)
 // 如果不是 lazy,則會立即執行一次
 if (!options.lazy) {
  effect()
 }
 return effect
}

// 建立 effect
function createReactiveEffect(
 fn: Function,
 options: any
): any {
 const effect = function effect(...args: any): any {
  return run(effect as any, fn, args)
 } as any
 effect.isEffect = true
 effect.active = true
 effect.raw = fn
 effect.scheduler = options.scheduler
 effect.onTrack = options.onTrack
 effect.onTrigger = options.onTrigger
 effect.onStop = options.onStop
 effect.computed = options.computed
 effect.deps = []
 return effect
}

// 執行函式,執行完之後會將儲存的 effect 刪除
// 這是函式 effect 的所有執行,所經歷的完整的宣告週期
function run(effect: any, fn: Function, args: any[]): any {
 if (!effect.active) {
  return fn(...args)
 }
 if (activeReactiveEffectStack.indexOf(effect) === -1) {
  try {
   activeReactiveEffectStack.push(effect)
   return fn(...args)
  } finally {
   activeReactiveEffectStack.pop()
  }
 }
}

一口氣寫了這麼多,最後總結一下。在大家看原始碼的時候,如果發現有哪個地方無從下手的話,可以先從測試用例開始看。因為測試用例可以很清楚的知道這個函式想要達到什麼效果,然後從效果上想,為什麼這麼做,如果我自己寫的話應該怎麼寫,這樣一點點就能揣摩出作者的意圖了。再根據原始碼結合自己的想法你就能夠學到很多。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援碼農教程。