最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3.0源码解析「reactive」篇 — 7.effect&trigger&track

    正文概述 掘金(鱼不想说话87743)   2021-08-19   849

    Effect

    学习 effect 源码,先从一个测试用例开始,首先我们构造了一个 counter 响应式代理,然后调用了 effect 函数传入一个会产生副作用的操作。此时 effect 中的函数会自动执行一遍,一是初始化了 dummy,二则是在 counter.num 的过程中了 get 拦截器里的 track 操作以跟踪依赖:

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

    Vue3.0源码解析「reactive」篇 — 7.effect&trigger&track

    再来回忆一下之前我们构造的响应式系统的几个概念:

    • effect:属性改变带来的副作用函数;
    • dep(依赖):effect副作用函数的集合;
    • depMap:通过属性 key 来映射其对应的依赖;
    • targetMap:被代理对象 targetdepMap 的集合;

    源码中的存储结构大同小异:

    // The main WeakMap that stores {target -> key -> dep} connections.
    // Conceptually, it's easier to think of a dependency as a Dep class
    // which maintains a Set of subscribers.
    type Dep = Set<ReactiveEffect>
    type KeyToDepMap = Map<any, Dep>
    const targetMap = new WeakMap<any, KeyToDepMap>()
    
    let activeEffect: ReactiveEffect | undefined
    

    Effect

    effect 函数很简单,内部调用 createReactiveEffect 构造一个上面所说的 ReactiveEffect 函数结构,如果传入的 options 没有 lazy 标志位则立刻执行返回的函数结构。

    如果传入的 fn 本身是一个 effect 结构,则解析出其 raw 中储存的原函数来构造一个新 ReactiveEffect

    export function effect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions = EMPTY_OBJ
    ): ReactiveEffect<T> {
      if (isEffect(fn)) {
        fn = fn.raw
      }
      const effect = createReactiveEffect(fn, options)
      if (!options.lazy) {
        effect()
      }
      return effect
    }
    

    createReactiveEffect

    createReactiveEffect 返回的是一个有很多标志位(额外属性)的二加工函数体,属性说明如下:

    export interface ReactiveEffect<T = any> {
      (): T  														// 自身的函数签名;
      _isEffect: true										// ReactiveEffect 标志位,用于 isEffect 检测;
      id: number												// effect 的自增 id(唯一标识);
      active: boolean										// ?
      raw: () => T											// 传入 effect 的原函数体缓存
      deps: Array<Dep>									// 包含这个 effect 的 dep 指针集,一个 effect 可能同时被多个 dep 包含。
      options: ReactiveEffectOptions		// 选项
      allowRecurse: boolean							// 是否允许递归
    }
    
    export function isEffect(fn: any): fn is ReactiveEffect {
      return fn && fn._isEffect === true
    }
    

    另外还要提及两个全局变量,uid 生产 effect 的自增 id0 开始,activeEffect

    let uid = 0
    let activeEffect: ReactiveEffect | undefined
    

    具体来看看 createReactiveEffect,先抛开 reactiveEffect 的函数体,直接看看后面为 effect 添加的属性,属性介绍在代码注释中写了。这里我们要知道这些属性是先被赋值,然后 effect 才被执行的。

    function createReactiveEffect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions
    ): ReactiveEffect<T> {
      const effect = function reactiveEffect(): unknown {
    		// 先忽略
      } as ReactiveEffect
      effect.id = uid++		// 自增 id,从 0 开始;
      effect.allowRecurse = !!options.allowRecurse
      effect._isEffect = true
      effect.active = true
      effect.raw = fn
      effect.deps = []
      effect.options = options
      return effect
    }
    

    回过头来分析 reactiveEffect 的函数体,这个函数其实还是很复杂的,因为它涉及到两个栈缓存 — effectStacktrackEffect

    Vue3.0源码解析「reactive」篇 — 7.effect&trigger&track

    为什么要通过 stack 的形式缓存 effect,这其实和 effect 的执行实际有关,如果 effect 不是 nested 的,那么从 effect 被创建到其被记录到某属性的 dep 的过程为下图:

    Vue3.0源码解析「reactive」篇 — 7.effect&trigger&track

    流程大概是 effect 创建一个 ReactiveEffect 结构体,然后将其缓存到一个全局变量 activeEffect 上。之后执行原函数 fnfn 中涉及 reactive get 的操作会触发拦截器,get proxy 中会将 activeEffect 记录到当前属性的 dep 上。

    Vue3.0源码解析「reactive」篇 — 7.effect&trigger&track

    如果说 effect 不是嵌套的,那这个流程没有任何问题,但是一旦执行的原函数里又执行了一个 effect 函数,那么原来记录的 activeEffect 就丢失了,所以为了解决嵌套 effect 调用的问题,vue 采用 effectStack 来缓存还没有添加依赖关系的 effect

    Vue3.0源码解析「reactive」篇 — 7.effect&trigger&track

    通过 effectStack 改写的执行流程如上图,reactiveEffect中还有另一个栈 trackEffect,这个栈用于控制标志位。

    function createReactiveEffect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions
    ): ReactiveEffect<T> {
      const effect = function reactiveEffect(): unknown {
        if (!effect.active) {
          // effect.active 这个表示最开始 true,代码走到这里说明我们调用了effect stop 函数;
          // 如果没有调度者,直接返回,否则直接执行fn
          return options.scheduler ? undefined : fn()
        }
        if (!effectStack.includes(effect)) {
          // 如果 effectStack 里面没有这个 effect,说明它是第一次被执行;
          cleanup(effect)
          try {
            enableTracking()
            effectStack.push(effect)
            activeEffect = effect
            return fn()
          } finally {
            effectStack.pop()
            resetTracking()
            activeEffect = effectStack[effectStack.length - 1]
          }
        }
      } as ReactiveEffect
    }
    

    cleanup 负责在每次 effect 执行时重新收集依赖,因为 effect 内的实际函数体 raw 可能会改变,原先依赖这个 effect 的属性 dep 可能会变化,所以这里就要先清空之前记录的 deps 指向,并且从所有原来记录的 dep 中删除这个 effect

    // 每次 effect 运行都会重新收集依赖, deps 是 effect 的依赖数组, 需要全部清空
    function cleanup(effect: ReactiveEffect) {
      const { deps } = effect
      if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
          deps[i].delete(effect)
        }
        deps.length = 0
      }
    }
    

    ReactiveEffectOptions

    创建 effect 的时候可以传入一个 option 属性做配置和调试,每个属性的解释如下:

    export interface ReactiveEffectOptions {
      lazy?: boolean														 // 是否延迟执行 effect
    	allowRecurse?: boolean										 // ? 暂时未知
      scheduler?: (job: ReactiveEffect) => void  // ? 暂时未知
      onTrack?: (event: DebuggerEvent) => void	 // track 触发时的回调函数
      onTrigger?: (event: DebuggerEvent) => void // trigger 触发时的回调函数
      onStop?: () => void												 // effect 监听停止时的回调函数
    }
    
    export type DebuggerEvent = {
      effect: ReactiveEffect
      target: object
      type: TrackOpTypes | TriggerOpTypes
      key: any
    } & DebuggerEventExtraInfo
    

    stop

    stop 函数用于清除某个 effect 副作用:

    export function stop(effect: ReactiveEffect) {
      if (effect.active) {
        cleanup(effect)
        if (effect.options.onStop) {
          effect.options.onStop()
        }
        effect.active = false
      }
    }
    

    track

    track 函数本身很简单,你给他一个 target 一个 key,他帮你把当前的 activeEffect 存储到 target->key->dep,然后在给你添加一个 effect -> [dep] 的反向映射。

    这里我们可以看到传递给 trackoption 其实没啥用,单纯用来 debugger 的文字枚举:

    export function track(target: object, type: TrackOpTypes, key: unknown) {
      if (!shouldTrack || activeEffect === undefined) {
        return
      }
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
      }
    }
    
    // using literal strings instead of numbers so that it's easier to inspect
    // debugger events
    export const enum TrackOpTypes {
      GET = 'get',
      HAS = 'has',
      ITERATE = 'iterate'
    }
    

    trigger

    触发依赖函数先创建一个 add 函数用来收集 active 或者 allowRecurse effects,之后对应不同的 TriggerOptiontarget 类型来收集 targetMap[target]上所有属性范围的依赖:

    • TriggerOpTypes.CLEAR: 对应 MAP 等类型的 clear 方法,直接收集所有依赖;
    • key === 'length' && isArray(target):改变数组长度属性,收集 length 属性以及 idx >= length 的所有索引属性的依赖。
    • key !== void 0:非长度为 0 方法,即
    export const enum TriggerOpTypes {
      SET = 'set',
      ADD = 'add',
      DELETE = 'delete',
      CLEAR = 'clear'
    }
    
    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown,
      oldTarget?: Map<unknown, unknown> | Set<unknown>
    ) {
      const depsMap = targetMap.get(target)
      if (!depsMap) {
        // never been tracked
        return
      }
    
      const effects = new Set<ReactiveEffect>()
      const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
        if (effectsToAdd) {
          effectsToAdd.forEach(effect => {
            if (effect !== activeEffect || effect.allowRecurse) {
              effects.add(effect)
            }
          })
        }
      }
    
      if (type === TriggerOpTypes.CLEAR) {
        // collection being cleared
        // trigger all effects for target
        depsMap.forEach(add)
      } else if (key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
          if (key === 'length' || key >= (newValue as number)) {
            add(dep)
          }
        })
      } else {
        // schedule runs for SET | ADD | DELETE
        if (key !== void 0) {
          add(depsMap.get(key))
        }
    
        // also run for iteration key on ADD | DELETE | Map.SET
        switch (type) {
          case TriggerOpTypes.ADD:
            if (!isArray(target)) {
              add(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            } else if (isIntegerKey(key)) {
              // new index added to array -> length changes
              add(depsMap.get('length'))
            }
            break
          case TriggerOpTypes.DELETE:
            if (!isArray(target)) {
              add(depsMap.get(ITERATE_KEY))
              if (isMap(target)) {
                add(depsMap.get(MAP_KEY_ITERATE_KEY))
              }
            }
            break
          case TriggerOpTypes.SET:
            if (isMap(target)) {
              add(depsMap.get(ITERATE_KEY))
            }
            break
        }
      }
    	// 依赖执行
    }
    

    依赖执行部分,先调用 effect.option 上的 onTrigger 回调做调试,然后如果 effectoptions.scheduler 属性说明是 computed 计算 构造的响应式,调用 scheduler 代替 effect 本身:

    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown,
      oldTarget?: Map<unknown, unknown> | Set<unknown>
    ) {
      // 收集依赖
    	const run = (effect: ReactiveEffect) => {
        if (__DEV__ && effect.options.onTrigger) {
          effect.options.onTrigger({
            effect,
            target,
            key,
            type,
            newValue,
            oldValue,
            oldTarget
          })
        }
        if (effect.options.scheduler) {
          effect.options.scheduler(effect)
        } else {
          effect()
        }
      }
    
      effects.forEach(run)
    }
    

    起源地下载网 » Vue3.0源码解析「reactive」篇 — 7.effect&trigger&track

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元