跳到主要內容

Vue 3.0 reactive, computed 原始碼分析


Vue 3.0 reactive, computed 原始碼分析 


此文件大量使用了程式碼,請善用 搜尋 ( Ctrl+F ) 來 trace 整段 code
在 Vue 3.0,computed 寫法也有所改變,寫法可以參考 Composition API,裡面提供的例子如下
const state = reactive({ count: 0, double: computed(() => state.count * 2) })

讓我們用 Test Case 來看看 computed 可以做什麼事情

computed 可以傳入一個 function,function 裡面的值只要有改變,或是被給值,就會更新 computed 的回傳值中的 value。
以下面的例子來說,可以發現 computed(() => value.foo) 裡面用到了 value.foo,所以當 value.foo = 1時,cValue中的 value 就會跟著更新為1
it('should return updated value', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo) expect(cValue.value).toBe(undefined) value.foo = 1 expect(cValue.value).toBe(1) })

Computed

知道了 computed 的功能之後,讓我們來追一下原始碼是怎麼讓 computed 更新的,還有其中的奧妙之處。
首先我們直接看進去 computed 這個 function 的實作。以原先的 Test Case 來說,可以直接看到傳入的方法符合 getterOrOptions: (() => T) | WritableComputedOptions<T> 這個 type,所以也就沒有 setter function,接下來就是第 23 行,宣告了一個 effect,注意此時是 lazy: true
export function computed<T>(getter: () => T): ComputedRef<T> export function computed<T>( options: WritableComputedOptions<T> ): WritableComputedRef<T> export function computed<T>( getterOrOptions: (() => T) | WritableComputedOptions<T> ): any { const isReadonly = isFunction(getterOrOptions) const getter = isReadonly ? (getterOrOptions as (() => T)) : (getterOrOptions as WritableComputedOptions<T>).get const setter = isReadonly ? __DEV__ ? () => { console.warn('Write operation failed: computed value is readonly') } : NOOP : (getterOrOptions as WritableComputedOptions<T>).set let dirty = true let value: T const runner = effect(getter, { lazy: true, // mark effect as computed so that it gets priority during trigger computed: true, scheduler: () => { dirty = true } })
在上一章我們已經稍微提到過effect的實際效果,我們來複習一下 code,如下。
可以發現 options.lazy 只要為 true 的話,則啟用的時間可以被外面的人控制。
export function effect( fn: Function, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if ((fn as ReactiveEffect).isEffect) { fn = (fn as ReactiveEffect).raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }
回到 computed,當 expect(cValue.value).toBe(undefined) 被執行時就會執行到此 getter function get value() ,而此時 dirty 為 true 時,runner 就會啟動,也就是 effect 就會開始進行蒐集依賴的動作。
蒐集依賴的方法上一章已經有提到,理論上就是 push activeReactiveEffectStack 、 run getter 、 pop activeReactiveEffectStack
這時候 expect(cValue.value).toBe(undefined) 執行完後,蒐集到了 value 這個 reactiveObject ,蒐集依賴的工作就告一段落。
const runner = effect(getter, { lazy: true, // mark effect as computed so that it gets priority during trigger computed: true, scheduler: () => { dirty = true } }) return { [refSymbol]: true, // expose effect so computed can be stopped effect: runner, get value() { if (dirty) { value = runner() dirty = false } // When computed effects are accessed in a parent effect, the parent // should track all the dependencies the computed property has tracked. // This should also apply for chained computed properties. trackChildRun(runner) return value }, set value(newValue: T) { setter(newValue) } } }
所以目前只要 value.foo = 1 這個 statement 被執行,觸發依賴就會啟動
,也就是先前講過的 trigger function ,如下。
看到 addRunners 這個 function 上,如果 effect 為 computed 就會將此 effect 加入 computedRunners Set 上,這是為了之後的 computedRunners 可以比一般的 effects 還要先執行,為什麼要這樣做呢?
因為當一般的 effect 需要使用到 computedObject 的任何資訊時,必須先讓 computedObject 中的 dirty 為 true,也就是優先跑 scheduler function ,這樣才能確保 effect 在後面跑的時候拿到的 computedObject 資訊是最新的。
export function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // never been tracked return } const effects = new Set<ReactiveEffect>() const computedRunners = new Set<ReactiveEffect>() if (type === OperationTypes.CLEAR) { // collection being cleared, trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { addRunners(effects, computedRunners, depsMap.get(key)) } // also run for iteration key on ADD | DELETE if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } // Important: computed effects must be run first so that computed getters // can be invalidated before any normal effects that depend on them are run. computedRunners.forEach(run) effects.forEach(run) } function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } }
離題了,這裡追蹤完後的結果, value.foo = 1 跑完之後,就會讓 cValue 的 dirty = true,那麼接下來的 expect(cValue.value).toBe(1) 這一行就會讓 cValue getter function 觸發並且更新且回傳最新的值。
這個 Test Case 就可以告一段落了。

Computed Chained

但如果牽扯到 computed chained 呢?
下面的 code 可以顯示出 c2 -> c1 -> value 的依賴關係,而 computed 的依賴建立只要記住一句話,都是由 getter function 開始蒐集,此例也就是 c2.value,此時就會執行 computedObject 中的 runner,由於這裡比較複雜,我用簡單的文字表達。
  1. push c2, activeReactiveEffectStack => [ c2 ]
  2. c2 run fn // which is c1.value + 1
  3. push c1, activeReactiveEffectStack => [ c2, c1 ]
  4. c1 run fn // which is value.foo
  5. value 依賴於 activeReactiveEffectStack top // 也就是 c1
  6. pop c1, activeReactiveEffectStack => [ c2 ]
  7. c1 run trackChildRun,將 activeReactiveEffectStack top 也就是 c2 依賴於 c1 的所有依賴
  8. pop c2, activeReactiveEffectStack => []
如此一來,父節點就會依賴於子節點的所有依賴點,而這是遞迴有效的,也就是說,父節點可以依賴於所有子孫節點上,且是攤平的依賴。
這樣的好處在於可以讓階層式的架構不會因為階層的多寡而導致依賴觸發的更新效能有所影響。
在此例中,value.foo++ 就足以讓 c1 與 c2 更新 dirty 為 true,了,所以不管 expect(c2.value).toBe(2) 與 expect(c1.value).toBe(1) 順序為何,兩邊的值都會更新。
it('should work when chained', () => { const value = reactive({ foo: 0 }) const c1 = computed(() => value.foo) const c2 = computed(() => c1.value + 1) expect(c2.value).toBe(1) expect(c1.value).toBe(0) value.foo++ expect(c2.value).toBe(2) expect(c1.value).toBe(1) })
function trackChildRun(childRunner: ReactiveEffect) { const parentRunner = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (parentRunner) { for (let i = 0; i < childRunner.deps.length; i++) { const dep = childRunner.deps[i] if (!dep.has(parentRunner)) { dep.add(parentRunner) parentRunner.deps.push(dep) } } } }

Triggered effect when chained

接下來來講一個當一般的 effect 依賴於 computed 時會發生什麼事情。
以此例來說,這時候的依賴為 effect -> c2 -> c1 -> value,effect 因為不是 lazy 所以會直接 run,以下直接用先前的文字順序表達
  1. push effect, activeReactiveEffectStack => [ effect ]
  2. effect run fn // which is dummy = c2.value
  3. push c2, activeReactiveEffectStack => [ effect, c2 ]
  4. c2 run fn // which is c1.value + 1
  5. push c1, activeReactiveEffectStack => [ effect, c2, c1 ]
  6. c1 run fn // which is value.foo
  7. value 依賴於 activeReactiveEffectStack top // 也就是 c1
  8. pop c1, activeReactiveEffectStack => [ effect, c2 ]
  9. c1 run trackChildRun,將 activeReactiveEffectStack top 也就是 c2 依賴於 c1 的所有依賴
  10. pop c2, activeReactiveEffectStack => [ effect ]
  11. c2 run trackChildRun,將 activeReactiveEffectStack top 也就是 effect 依賴於 c2 的所有依賴 (而又此時 c2 擁有 c1 的所有依賴,所以也就是 c1, c2 兩者的所有依賴
  12. pop effect, activeReactiveEffectStack => []
這時候就很簡單了,dummy 在 12 行當然會是 1, getter1 與 getter2 也都執行一次,接著 value.foo++ ,觸發了三個依賴者 effect 、 c2 、 c1,而當然 computedRunners 與 effects 都是集合,所以必定只會執行一次,也都會觸發各自的 function,這個 Test Case 就完成了。
it('should trigger effect when chained', () => { const value = reactive({ foo: 0 }) const getter1 = jest.fn(() => value.foo) const getter2 = jest.fn(() => { return c1.value + 1 }) const c1 = computed(getter1) const c2 = computed(getter2) let dummy effect(() => { dummy = c2.value }) expect(dummy).toBe(1) expect(getter1).toHaveBeenCalledTimes(1) expect(getter2).toHaveBeenCalledTimes(1) value.foo++ expect(dummy).toBe(2) // should not result in duplicate calls expect(getter1).toHaveBeenCalledTimes(2) expect(getter2).toHaveBeenCalledTimes(2) })
這樣就可以解釋所有 computed 的情況是多麼複雜與艱辛,但困難的還在後頭,之後這個系列的文章會慢慢地往 core 或是 virtual-dom 的地方邁進。

留言