vue3 effect

Learn vue3 effect from test cases

This sharing is mainly about effectsome functions, options and implementation principles of this API.

The core content is how effect tracks changes within reactive

test case

Because there are more than 700 lines of test cases in the effect.spec.ts file, some of them are omitted later. I won’t go into details this time. You can check and study them by yourself.

The following content comes from vue-next/packages/reactivity/ tests /effect.spec.ts

  1. Number of verification calls

    it('should run the passed function once (wrapped by a effect)', () => {   
        const fnSpy = jest.fn(() => {})
        effect(fnSpy) // 验证这里会立即执行一次函数
        expect(fnSpy).toHaveBeenCalledTimes(1)
      })
    
  2. The function of verifying basic response attributes
    is probably the core of effect. How to track attribute changes and call callback functions is the main content of this sharing

    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. About some options and other functions

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

    A simple analysis of the principle of effect tracking

    Let's recall first, in the last sharing about reactive, after creating, an operation proxywill be performed on :targetMapset

    The following content comes from: 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
    }
    

    Then handlersthere are calls in trackor trigger.

    For example, the objects Getand Set:

    The following content comes from: 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
    }
    
    A brief description of the effect implementation process
    1. Create a reactive object =>obj
    2. Call effectand pass the callback function. The callback involves objthe get operation, such asobj.a
    3. effect internal executioncreateReactiveEffect
    4. createReactiveEffect adds some effect attributes internally, then returns a function, and returns a run function inside the function.
    5. The run function determines whether the effectStack contains the current effect, if not, adds it, and then executes the callback passed in the second step.
    6. At this point createReactiveEffect, the execution is completed, and the effect internally determines whether to execute the returned function option.lazyimmediately (it will be executed later)createReactiveEffect
    7. The callback function is called. obj.aWhen it is executed, the proxy's get processing method is started, and trackthe method is called.
    8. track finds the current effect based on effectStack (the implementation here is relatively seconds, which will be introduced later), then finds targetMapthe map, and finally puts the current effect function into it.
    9. When obj.achanges occur, execution beginstrigger
    10. The trigger function is equivalent to the reverse operation of track, which is taken out and executed (of course, it is actually much more complicated than track)
    Source code level understanding

    The following is displayed in sequence according to the above steps, the content comes from 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()
      }
    }
    

Guess you like

Origin blog.csdn.net/kang_k/article/details/105860413