從測試用例來學習vue3 effect
此次分享主要是 effect
這個 API 的一些功能、option 以及實現原理。
核心內容是 effect 如何做到跟蹤 reactive 內的變化的
測試用例
因爲 effect.spec.ts 文件中的測試用例 700 多行,所以後面省略了一些,此次不細說,大家自行查看研究。
以下內容來自vue-next/packages/reactivity/tests/effect.spec.ts
-
驗證調用次數
it('should run the passed function once (wrapped by a effect)', () => { const fnSpy = jest.fn(() => {}) effect(fnSpy) // 驗證這裏會立即執行一次函數 expect(fnSpy).toHaveBeenCalledTimes(1) })
-
驗證基礎響應屬性
這塊的功能大概就是 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) }) // 多個 reactive 屬性 it('should observe multiple properties', () => { let dummy const counter = reactive({ num1: 0, num2: 0 }) effect(() => (dummy = counter.num1 + counter.num1 + counter.num2)) expect(dummy).toBe(0) counter.num1 = counter.num2 = 7 expect(dummy).toBe(21) }) // 觸發多個 effect it('should handle multiple effects', () => { let dummy1, dummy2 const counter = reactive({ num: 0 }) effect(() => (dummy1 = counter.num)) effect(() => (dummy2 = counter.num)) expect(dummy1).toBe(0) expect(dummy2).toBe(0) counter.num++ expect(dummy1).toBe(1) expect(dummy2).toBe(1) }) // 嵌套 it('should observe nested properties', () => { let dummy const counter = reactive({ nested: { num: 0 } }) effect(() => (dummy = counter.nested.num)) expect(dummy).toBe(0) counter.nested.num = 8 expect(dummy).toBe(8) }) // 刪除屬性 it('should observe delete operations', () => { let dummy const obj = reactive({ prop: 'value' }) effect(() => (dummy = obj.prop)) expect(dummy).toBe('value') delete obj.prop expect(dummy).toBe(undefined) }) // 刪除後 再添加,驗證 has 方法的響應 it('should observe has operations', () => { let dummy const obj = reactive<{ prop: string | number }>({ prop: 'value' }) effect(() => (dummy = 'prop' in obj)) expect(dummy).toBe(true) delete obj.prop expect(dummy).toBe(false) obj.prop = 12 expect(dummy).toBe(true) }) // 原型鏈上的屬性響應測試 it('should observe properties on the prototype chain', () => { let dummy const counter = reactive({ num: 0 }) const parentCounter = reactive({ num: 2 }) // 設置原型對象 Object.setPrototypeOf(counter, parentCounter) effect(() => (dummy = counter.num)) expect(dummy).toBe(0) // 刪除自身的 num 屬性 delete counter.num expect(dummy).toBe(2) // 測試原型上的 num parentCounter.num = 4 expect(dummy).toBe(4) // 又添加回來了 counter.num = 3 expect(dummy).toBe(3) }) // 和上面大致相同 it('should observe has operations on the prototype chain', () => { let dummy const counter = reactive({ num: 0 }) const parentCounter = reactive({ num: 2 }) Object.setPrototypeOf(counter, parentCounter) effect(() => (dummy = 'num' in counter)) expect(dummy).toBe(true) delete counter.num expect(dummy).toBe(true) delete parentCounter.num expect(dummy).toBe(false) counter.num = 3 expect(dummy).toBe(true) }) // 測試原型上的屬性修飾方法 it('should observe inherited property accessors', () => { let dummy, parentDummy, hiddenValue: any const obj = reactive<{ prop?: number }>({}) const parent = reactive({ set prop(value) { hiddenValue = value }, get prop() { return hiddenValue } }) Object.setPrototypeOf(obj, parent) effect(() => (dummy = obj.prop)) effect(() => (parentDummy = parent.prop)) expect(dummy).toBe(undefined) expect(parentDummy).toBe(undefined) obj.prop = 4 expect(dummy).toBe(4) // 這裏的 parent.prop === 4 但 parentDummy === undefined // this doesn't work, should it? // expect(parentDummy).toBe(4) parent.prop = 2 expect(dummy).toBe(2) expect(parentDummy).toBe(2) }) // 此次省略 N 多測試用例 //關於這個測試用例比較有意思 it('should observe json methods', () => { let dummy = <Record<string, number>>{} const obj = reactive<Record<string, number>>({}) effect(() => { // 通過 json 轉換 dummy = JSON.parse(JSON.stringify(obj)) }) obj.a = 1 // 這裏依舊可以跟蹤到 expect(dummy.a).toBe(1) })
-
關於一些 option 及其他功能
// options.lazy it('lazy', () => { const obj = reactive({ foo: 1 }) let dummy const runner = effect(() => (dummy = obj.foo), { lazy: true }) expect(dummy).toBe(undefined) // 需要手動執行一次纔可以跟蹤到 expect(runner()).toBe(1) expect(dummy).toBe(1) obj.foo = 2 expect(dummy).toBe(2) }) // options.scheduler it('scheduler', () => { let runner: any, dummy const scheduler = jest.fn(_runner => { runner = _runner }) const obj = reactive({ foo: 1 }) effect( () => { dummy = obj.foo }, { scheduler } ) expect(scheduler).not.toHaveBeenCalled() // 傳入了 scheduler,第一次會默認執行一次 expect(dummy).toBe(1) // should be called on first trigger obj.foo++ expect(scheduler).toHaveBeenCalledTimes(1) // should not run yet expect(dummy).toBe(1) // manually run runner() // should have run expect(dummy).toBe(2) }) // options.onTrack it('events: onTrack', () => { let events: DebuggerEvent[] = [] let dummy const onTrack = jest.fn((e: DebuggerEvent) => { events.push(e) }) const obj = reactive({ foo: 1, bar: 2 }) const runner = effect( () => { // 這裏執行 get has 都會調用一次 track dummy = obj.foo dummy = 'bar' in obj dummy = Object.keys(obj) }, { onTrack } ) expect(dummy).toEqual(['foo', 'bar']) // 注意這裏 onTrack 被執行了 3 次 expect(onTrack).toHaveBeenCalledTimes(3) expect(events).toEqual([ { effect: runner, target: toRaw(obj), type: OperationTypes.GET, key: 'foo' }, { effect: runner, target: toRaw(obj), type: OperationTypes.HAS, key: 'bar' }, { effect: runner, target: toRaw(obj), type: OperationTypes.ITERATE, key: ITERATE_KEY } ]) }) // options.onTrigger it('events: onTrigger', () => { let events: DebuggerEvent[] = [] let dummy const onTrigger = jest.fn((e: DebuggerEvent) => { events.push(e) }) const obj = reactive({ foo: 1 }) const runner = effect( () => { dummy = obj.foo }, { onTrigger } ) obj.foo++ expect(dummy).toBe(2) expect(onTrigger).toHaveBeenCalledTimes(1) expect(events[0]).toEqual({ effect: runner, target: toRaw(obj), type: OperationTypes.SET, key: 'foo', oldValue: 1, newValue: 2 }) delete obj.foo expect(dummy).toBeUndefined() expect(onTrigger).toHaveBeenCalledTimes(2) expect(events[1]).toEqual({ effect: runner, target: toRaw(obj), type: OperationTypes.DELETE, key: 'foo', oldValue: 2 }) }) // stop 不是在 options 傳遞的,額外的一個方法 it('stop', () => { let dummy const obj = reactive({ prop: 1 }) const runner = effect(() => { dummy = obj.prop }) obj.prop = 2 expect(dummy).toBe(2) stop(runner) obj.prop = 3 expect(dummy).toBe(2) // stopped effect should still be manually callable runner() expect(dummy).toBe(3) }) // options.onTrigger it('events: onStop', () => { const onStop = jest.fn() const runner = effect(() => {}, { onStop }) stop(runner) expect(onStop).toHaveBeenCalled() }) // stop 後恢復之前的自動跟蹤 it('stop: a stopped effect is nested in a normal effect', () => { let dummy const obj = reactive({ prop: 1 }) const runner = effect(() => { dummy = obj.prop }) stop(runner) obj.prop = 2 expect(dummy).toBe(1) // observed value in inner stopped effect // will track outer effect as an dependency // 將 runner 重新放入 effect 中,相當於 dummy = obj.prop 又一次被跟蹤 effect(() => { runner() }) expect(dummy).toBe(2) // notify outer effect to run obj.prop = 3 expect(dummy).toBe(3) }) // reactive 被標記了 markNonReactive 不會響應 it('markNonReactive', () => { const obj = reactive({ foo: markNonReactive({ prop: 0 }) }) let dummy effect(() => { dummy = obj.foo.prop }) expect(dummy).toBe(0) obj.foo.prop++ expect(dummy).toBe(0) obj.foo = { prop: 1 } expect(dummy).toBe(1) }) // 設置 NaN 不會被多次觸發跟蹤回調 it('should not be trigger when the value and the old value both are NaN', () => { const obj = reactive({ foo: NaN }) const fnSpy = jest.fn(() => obj.foo) effect(fnSpy) obj.foo = NaN expect(fnSpy).toHaveBeenCalledTimes(1) }) })
effect 實現跟蹤的原理簡單分析
先回憶一下,上次關於 reactive 的分享中,在創建
proxy
後會對targetMap
來一個set
操作:以下內容來自: vue-next/packages/reactivity/src/reactive.ts
function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { // ... 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)) { // 注意這裏 set 了一個 map targetMap.set(target, new Map()) } return observed }
然後
handlers
中都有調用track
或trigger
。比如對象的
Get
和Set
:以下內容來自: vue-next/packages/reactivity/src/baseHandlers.ts
function createGetter(isReadonly: boolean) { return function get(target: object, key: string | symbol, receiver: object) { // ... track(target, OperationTypes.GET, key) // 這個 track 是引用的 effect.ts // ... } } function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { // ... // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { /* istanbul ignore else */ // 這裏區分開發環境與生產環境,主要是了 debug 使用(vue-tools 留接口?) if (__DEV__) { const extraInfo = { oldValue, newValue: value } if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) } else if (hasChanged(value, oldValue)) { trigger(target, OperationTypes.SET, key, extraInfo) } } else { if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (hasChanged(value, oldValue)) { trigger(target, OperationTypes.SET, key) } } } return result }
effect 實現流程簡要描述
- 創建一個 reactive 對象 =>
obj
- 調用
effect
,傳遞迴調函數,回調中涉及到了obj
的 get 操作,比如obj.a
- effect 內部執行
createReactiveEffect
- createReactiveEffect 內部添加一些 effect 的屬性,然後 return 一個函數,函數內部又 return 了一個 run 函數
- run 函數判斷 effectStack 是否包含了當前的 effect,如果沒有則添加,然後執行第二步傳遞的回調
- 到這裏
createReactiveEffect
執行完畢了,effect 內部判斷option.lazy
是否立即執行一次createReactiveEffect
返回的函數(後面就當執行了) - 回調函數被調用了,執行到
obj.a
的時候,就出發了 proxy 的 get 處理方法,會調用track
方法 - track 根據 effectStack 找到當前的 effect(這裏實現的比較秒,後面介紹),然後去
targetMap
找 map,最後把當前的 effect 函數 賽進去 - 當
obj.a
發生變化後,開始執行trigger
- trigger 函數相當於 track 的反操作,取出來然後執行(當然,實際是要比 track 複雜的多的多)
源碼層面的理解
以下按照上面的步驟依次展示,內容來自 vue-next/packages/reactivity/src/effect.ts
// 對外 API,effect 函數 export function effect<T = any>( // 回調 fn: () => T, // 配置項 options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { // 這裏判斷傳入的 fn 是否已經是一個 effect 了。如果是獲取源回調函數, if (isEffect(fn)) { fn = fn.raw } // 創建 effect,傳入 回調與配置項 const effect = createReactiveEffect(fn, options) // 判斷是否開啓了 lazy 模式 if (!options.lazy) { // 沒有開啓,立即執行 effect effect() } return effect } // createReactiveEffect 函數 function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { // 這裏 effect 被賦值了一個函數 const effect = function reactiveEffect(...args: unknown[]): unknown { // 執行 reactiveEffect 後會 執行 run ,同時傳遞 effect, fn, args return run(effect, fn, args) } as ReactiveEffect // 給 effect 設置自身屬性 effect._isEffect = true effect.active = true effect.raw = fn // 這裏會緩存 track 的列表,後續用於 stop 將自己刪除掉 effect.deps = [] effect.options = options return effect } // run 函數 function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown { // 判斷是否被 stop 了 // 被 stop 後,只有主動調用 runner(調用 effect 返回的函數)纔會走到這裏 if (!effect.active) { return fn(...args) } // 判斷響應隊列是包含了 當前的 effect if (!effectStack.includes(effect)) { // 不是太理解 爲什麼每次都要 cleanup? cleanup(effect) try { // 注意這裏 push 了一次 effect effectStack.push(effect) return fn(...args) } finally { // 等 fn 執行完後,立刻取出 effect effectStack.pop() } } } // 假設 effect 已調用一次,那麼這裏已經觸發了 get 代理事件,也就是執行了 track 函數 export function track(target: object, type: OperationTypes, key?: unknown) { // 判斷 effectStack 隊列是否有值 // shouldTrack 應該是開放模式下 vue-tools 可以暫停跟蹤的功能 if (!shouldTrack || effectStack.length === 0) { return } // 注意這裏 爲什麼是 effectStack[effectStack.length - 1] // 最後一個就是當前的 effect 嗎? const effect = effectStack[effectStack.length - 1] if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } // 從 targetMap 中取值,用於添加涉及到的 effect!!! let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } // targetMap 是 weekMap // depsMap 是 Map // dep 是 Set() // 這裏有個 key,是 target 上面的屬性,所以這裏的 effect 存儲也按對應的字段區分開了 let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } // 將當前 effect 放入, 供後面 trigger 使用 if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.options.onTrack) { effect.options.onTrack({ effect, target, type, key }) } } } // const effect = effectStack[effectStack.length - 1] 說明 // 因爲上面 run 函數中 push 之後,立刻執行了 fn(),fn 中又觸發了 代理鉤子 // 代理鉤子中又調用了 track // 因爲 js 單線程的機制,effectStack.length - 1 永遠會是 run 函數中 push 的那一個 // trigger 函數 export function trigger( target: object, type: OperationTypes, key?: unknown, extraInfo?: DebuggerEventExtraInfo ) { // 從 targetMap 中獲取 depsMap const depsMap = targetMap.get(target) // 表示沒有被 effect 調用過 if (depsMap === void 0) { // never been tracked return } // 涉及到的 effect 存放(比如有獲取 obj.a 操作的 effect) const effects = new Set<ReactiveEffect>() // 計算屬性。 const computedRunners = new Set<ReactiveEffect>() // 下面會根據 key 獲取對應的 effect // clear 的時候 不區分字段了 all in(數組的時候纔會有CLEAR) if (type === OperationTypes.CLEAR) { // collection being cleared, trigger all effects for target depsMap.forEach(dep => { // addRuners 只是取出 effect 放入到 effects 和 computedRunners,因爲內部有 if 邏輯,所以抽出來一個函數 addRunners(effects, computedRunners, dep) }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { // 只取出 key 對應的 effect 放入對應的集合 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)) } } // 使用調度器 運行傳入的 effect 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 要先運行 computedRunners.forEach(run) effects.forEach(run) } // 只是取出 effect 放入到 effects 和 computedRunners function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.options.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } } // 調度器 function scheduleRun( effect: ReactiveEffect, target: object, type: OperationTypes, key: unknown, extraInfo?: DebuggerEventExtraInfo ) { if (__DEV__ && effect.options.onTrigger) { const event: DebuggerEvent = { effect, target, key, type } effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event) } // 很簡單,傳遞調度器了,調用調度器函數,否則 執行 effect if (effect.options.scheduler !== void 0) { effect.options.scheduler(effect) } else { effect() } }
- 創建一個 reactive 對象 =>