跳到主要內容

Vue 3.0 reactive, effect 到底做了什麼事

 

Vue 3.0 reactive, effect 到底做了什麼事


相信 vue-next (vue 3.0) 出來之後大家對 source code 都非常好奇,且 vue 3.0 也改為 Composition API,當然大家最好奇的應該還是 Vue 3.0 到底對於 Proxy API 用到了怎樣的境界,所以本篇主軸會在於 vue 3.0 reative 到底做了什麼事情

凡事先看 test case

由於 vue 3.0 更換成了 Composition API,強烈建議先看完 Composition API 再來往下探討
以下圖 unit testing 的程式碼來說,可以看到 他將 { num: 0 } 設為 reactive,並且只要設定 num 的值,第四行的 () => (dummy = counter.num) 就會被 call。
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) })

reactive 實作

可以得知 reactive 這個 function 會回傳一個 reactive object,
export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (readonlyToRaw.has(target)) { return target } // target is explicitly marked as readonly by user if (readonlyValues.has(target)) { return readonly(target) } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ) }
createReactiveObject 當然先判斷 target 是否已經有 observe 或者本身就是 proxy。接下來就是針對 target.constructor 如果是 Set, Map, WeakMap, WeakSet 就使用 collectionHandlers ,反之為 baseHandlers ,以這個 test cast 的例子會使用 mutableHandlers。
function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // target already has corresponding Proxy let observed = toProxy.get(target) if (observed !== void 0) { return observed } // target is already a Proxy if (toRaw.has(target)) { return target } // only a whitelist of value types can be observed. if (!canObserve(target)) { return target } const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed }
接著我們往下看 mutableHandlers ,依照先前給的值,mutableHandlers 勢必是要一個 proxy handlers,而依照 vue2.x 的經驗,都是 getter 在專門收集依賴,所以繼續往下看。
export const mutableHandlers: ProxyHandler<any> = { get: createGetter(false), set, deleteProperty, has, ownKeys }
這邊可以看到正常情況下,會跑到 track 這個 function 上,且跟以前一樣,如果偵測到 res 是 Object 的話,就會遞迴的往下對他進行 reactive 操作
function createGetter(isReadonly: boolean) { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) if (typeof key === 'symbol' && builtInSymbols.has(key)) { return res } if (isRef(res)) { return res.value } track(target, OperationTypes.GET, key) return isObject(res) ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res } }
我們可以看到 track function 裡面有一個操作類似於 Vue 2.x 的 Dep.target stack, 即為 activeReactiveEffectStack,在這裡會取出最後一個 effect 進行 targetMap 的設置。而 targetMap 是一個 WeakMap
,總之就是存該 target 中的所有 deps,接著就將 effect 推入一個 deps 上。
  • targetMap = target 對應 depsMap
  • depsMap = key 對應 deps
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
export function track( target: any, type: OperationTypes, key?: string | symbol ) { if (!shouldTrack) { return } const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (effect) { if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.onTrack) { effect.onTrack({ effect, target, type, key }) } } } }
但這時候可能大家會納悶,那 activeReactiveEffectStack 這個東西是從哪來的呢?
還記得我們 effect(() => (dummy = counter.num)) 這一段 code 吧。
讓我們看一下 effect 的實作細節。可以清楚的看到在沒有 lazy 的情況底下會直接 call createReactiveEffect(fn, options) 的 return function。
也就是這一段 run(effect as ReactiveEffect, fn, args)
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 }
function createReactiveEffect( fn: Function, options: ReactiveEffectOptions ): ReactiveEffect { const effect = function effect(...args): any { return run(effect as ReactiveEffect, fn, args) } as ReactiveEffect 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 }
而 run 當中就會看到關鍵的 stack push activeReactiveEffectStack.push(effect),與 return fn(...args) 然後當然 finally 中的 stack pop activeReactiveEffectStack.pop()
這裡要了解一件事情,就是 finally block 總是會執行,就算是在 return 後面
function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if (!effect.active) { return fn(...args) } if (activeReactiveEffectStack.indexOf(effect) === -1) { cleanup(effect) try { activeReactiveEffectStack.push(effect) return fn(...args) } finally { activeReactiveEffectStack.pop() } } }
好,該了解的都了解完了,讓我們回頭看看這個 test case,reactive 會怎麼完成他要做的工作
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) })
首先可以看到,第 3 行將{ num: 0 }轉為 reactive。
依照前面的例子所見 counter 將會是 createReactiveObject 的回傳值,也就是
new Proxy({ num: 0 }, mutableHandlers)
這時候到第四行,我們可以知道 effect 裡面傳了一個 function 為 () => (dummy = counter.num),且不為 lazy。
所以 effect 會直接 call,也就是會執行下面這行
const effect = function effect(...args): any { return run(effect as ReactiveEffect, fn, args) } as ReactiveEffect
當 run 跑進去後,我們知道他總共會做三件事情
  1. push activeReactiveEffectStack
  2. run fn
  3. pop activeReactiveEffectStack
所以跑到第二步時,stack 上已經有一個 effect 的資料,且 fn 為 dummy = counter.num,這時候就會 call 到 counter 的 proxy getter function。
還記得會發生什麼事情嗎,就是會跑到 track function,
而如果你還記得的話 (我相信到這裏大家都忘光了),track 會將 stack 的 top effect 拿出來建立 target 某個 key 與該 effect 的對應關係。
好,到這裡已經快結束了,但少了一個東西,我們前面都沒有提到過,就是 set 會發生什麼事情
counter.num = 7
首先讓我們看一下 set handler 的 code
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean { value = toRaw(value) const hadKey = hasOwn(target, key) const oldValue = target[key] if (isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { /* istanbul ignore else */ if (__DEV__) { const extraInfo = { oldValue, newValue: value } if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key, extraInfo) } } else { if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key) } } } return result }
一般來說當 proxy 代理一個陣列時,做基本的 push 時可能會觸發很多次的 get 與 set ( set value , set length ),如果沒有多做一些處理,很有可能會執行很多次的 trigger。
這裡作者有一些小智慧在裏頭,假如現在 push 了'world'進入了 ['hello'],這時候會先做set [1] = "world",此時 hadKey 為 false 這就會一路走到 trigger(target, OperationTypes.ADD, key, extraInfo),接著 set length = 2,此時 hadKey 是 true 的,且 value是與oldValue相等的,所以可以完美地躲掉執行很多次 trigger 的問題
回到主題上,那 trigger 又會做什麼事情呢?
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: Set<ReactiveEffect> = new Set() const computedRunners: Set<ReactiveEffect> = new Set() 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) }
當然聰明的你應該都知道該怎麼做了,就是把 targetMap 對照表拿出來,然後對他所有的 effect 做一次 run 就好了。
當然這也只是 reactive 的皮毛而已,下一篇我會討論 computedRunners 的實作方法與 readonly 特性。

留言