vue3 effect

從測試用例來學習vue3 effect

此次分享主要是 effect 這個 API 的一些功能、option 以及實現原理。

核心內容是 effect 如何做到跟蹤 reactive 內的變化的

測試用例

因爲 effect.spec.ts 文件中的測試用例 700 多行,所以後面省略了一些,此次不細說,大家自行查看研究。

以下內容來自vue-next/packages/reactivity/tests/effect.spec.ts

  1. 驗證調用次數

    it('should run the passed function once (wrapped by a effect)', () => {   
        const fnSpy = jest.fn(() => {})
        effect(fnSpy) // 驗證這裏會立即執行一次函數
        expect(fnSpy).toHaveBeenCalledTimes(1)
      })
    
  2. 驗證基礎響應屬性
    這塊的功能大概就是 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)
      })
    
  3. 關於一些 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 中都有調用 tracktrigger

    比如對象的 GetSet

    以下內容來自: 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 實現流程簡要描述
    1. 創建一個 reactive 對象 => obj
    2. 調用 effect,傳遞迴調函數,回調中涉及到了obj 的 get 操作,比如 obj.a
    3. effect 內部執行 createReactiveEffect
    4. createReactiveEffect 內部添加一些 effect 的屬性,然後 return 一個函數,函數內部又 return 了一個 run 函數
    5. run 函數判斷 effectStack 是否包含了當前的 effect,如果沒有則添加,然後執行第二步傳遞的回調
    6. 到這裏 createReactiveEffect 執行完畢了,effect 內部判斷 option.lazy 是否立即執行一次 createReactiveEffect返回的函數(後面就當執行了)
    7. 回調函數被調用了,執行到 obj.a 的時候,就出發了 proxy 的 get 處理方法,會調用 track 方法
    8. track 根據 effectStack 找到當前的 effect(這裏實現的比較秒,後面介紹),然後去 targetMap 找 map,最後把當前的 effect 函數 賽進去
    9. obj.a 發生變化後,開始執行 trigger
    10. 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()
      }
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章