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()
      }
    }
    

猜你喜欢

转载自blog.csdn.net/kang_k/article/details/105860413