vue3学习(5)effect源码阅读理解

607 阅读8分钟

前言:effect类似于vue2源码中的watch,观察者/订阅者。 以下过程中,effect为观察者,target的属性值为被观察者,effect观察target的属性值,target的属性值被修改通知effect,确保以这个思想看待下面源码。

副作用effect

副作用其实是一个比较生僻的概念,最早来自于“纯函数”,纯函数是编程界早期的一个概念,具体可以看这里这里。 纯函数特征:

  • 它应始终返回相同的值。不管调用该函数多少次,无论今天、明天还是将来某个时候调用它。
  • 自包含(不使用全局变量)。
  • 它不应修改程序的状态或引起副作用(修改全局变量)。 所以,纯函数的副作用就是:

一个函数除了返回确定的值之外,还做了其他的事情,那么这个函数做的这些事情就叫做“副作用”。

比如console.log(123)就有副作用,它返回undefined是主作用,但是我们不需要它的主作用,它的副作用就是在控制台打印123,我们要的是它的副作用。

Vue 3里的副作用概念 所以首先了解一下“主作用”,在Vue世界里,视图层和DOM层是两码事,尽管一些人认为它们是一码事。变更响应式数据的主作用就是变更后的数据能渲染到视图层。但是基本这就是前端最重要的事,副作用就是响应式数据的变更造成的其他连锁反应,以及后续逻辑,这些连锁反应都叫副作用。在药物学里,副作用往往是不良反应,但是在Vue3里并不是。上面标题里说“清除副作用”,也并不是说因为副作用是不良反应所以要清除,而是Vue3提供一个方法让你随时可以取消副作用。

副作用主要有:DOM更新,watchEffectwatchcomputed...

Effect与Target映射

// 存放effect集合,使用Set去重
type Dep = Set<ReactiveEffect>
// any为监控对象的属性,属性对应的Dep
// 存放监控对象所有属性与对应Dep
type KeyToDepMap = Map<any, Dep>
// any为target
const targetMap = new WeakMap<any, KeyToDepMap>()

上述代码目的:使用Proxy监控是target,每次触发handler都是属性维度,因此需要维护属性与effect集合的映射。

Dep == 》effect集合

keyToDepMap ===》 target的所有属性对应Dep

targetMap ===》target对应的所有属性监控

修改target对应属性,可以通过以上映射找到该属性对应的effect进行触发执行。

effect的类型

export interface ReactiveEffect<T = any> {
  (): T
  // effect的标志属性
  _isEffect: true
  // 唯一标识
  id: number
  active: boolean
  // 原始函数
  raw: () => T
  // 关联effect的响应数据的观察者集合
  // effect与对应的响应数据指向的观察者集合是同一个对象
  // 目的就是为了当该effect需要与响应数据分开时,直接从deps中移除该effect,同时对应的响应数据的观察者集合也移除了该effect,因为指向同一份数据
  deps: Array<Dep>
  // effect的配置选项
  options: ReactiveEffectOptions
}
// active属性作用是当为true时,会清楚effect的依赖并且执行onStop事件
// stop会在unmountComponent内部进行触发
// 因此是组件消亡的时候会触发stop,如果active为true会清除依赖触发onStrop
stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false
  }
}

可以从后面的effect创建函数里面了解到,activefalse时,effect就是一个普通的传入函数或undefined,没有监听的依赖项

effect的配置属性

export interface ReactiveEffectOptions {
  // 是否立即执行
  lazy?: boolean
  // 调度器,是否自定义调度effect
  scheduler?: (job: ReactiveEffect) => void
  // 是否收集的监听函数
  onTrack?: (event: DebuggerEvent) => void
  // 是否触发的监听函数
  onTrigger?: (event: DebuggerEvent) => void
  // 是否停止的监听函数
  onStop?: () => void
}
// 事件类型
export type DebuggerEvent = {
  // effect
  effect: ReactiveEffect
  // 监听对象
  target: object
  // 类型,这个主要是看何种方式,例如ADD、DELETE、CLEAR
  type: TrackOpTypes | TriggerOpTypes
  // porxy监听的属性
  key: any
} & DebuggerEventExtraInfo
export interface DebuggerEventExtraInfo {
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

effect构造函数

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 如果fn是监听函数,则获取原始函数,重新创建
  // 从这可以看出,effect每次会返回一个新的监控函数
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 核心就是createReactiveEffect
  const effect = createReactiveEffect(fn, options)
  // lazy属性就是标识是否默认执行一次
  if (!options.lazy) {
    effect()
  }
  return effect
}

传入一个函数,返回一个effect的构造函数

3.createReactiveEffect函数

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // 创建ReactiveEffect类型的函数
  const effect = function reactiveEffect(): unknown {
    // 如果active为false,则不会进行依赖收集
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    // effectStack指的是effect栈
    // 如果effectStack中没有effect,则进行执行
	// 为了防止循环依赖
	// effect1中触发effect2,此时触发effect2的调用,此时effect2中又触发effect1,但是此时effect1还在effectStack中,则不会进入
    if (!effectStack.includes(effect)) {
      // 清除effect中对应的监控数据
      // 并且从监控数据依赖中清除该effect观察者
      // 双向进行清除,主要通过deps属性
      cleanup(effect)
      try {
        // 向trackStack推入一个shouldTrack
        // shouldTrack主要是是否进行依赖收集标志位
        enableTracking()
        // 向effectStack栈推入一个effect
        effectStack.push(effect)
        // 活动effect指向该effect
        activeEffect = effect
        // 执行fn,进行触发内部监控数据对应的getter来收集该effect依赖
        return fn()
      } finally {
        // 弹出该effect
        effectStack.pop()
        // 弹出shouldTrack
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  // 设置effect的基础属性
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
// 记录上一次的shouldTrack状态
// 并且现在置为false
// 例如当wraning时会执行该函数
export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}
// 记录上一次的shouldTrack状态
// 并且现在依赖可进行收集,因为要执行fn,获取
export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}
// effect执行完毕,则将shoudTrack置为之前状态
export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

将原始函数fn重新定义为effect,主要作用类似watch当作为观察者

cleanup函数

作用:为了清除该effect依赖项

function cleanup(effect: ReactiveEffect) {
  // 获取与该effect有关的被观察者的effect集合
  // 从前面可知,该effect关联的监控对象指向的dep是在同一个存储地址
  const { deps } = effect
  if (deps.length) {
    // 从dep中删除该effect
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    // 并且去除deps的每一项
    deps.length = 0
  }
}

deps[i].delete操作切除了被观察与effect的联系,deps.length=0,操作切除了effect与被观察的联系。

track函数

作用:依赖收集

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果不进行依赖收集或目标观察者为undefined则直接返回
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 获取该target的属性与观察者收集箱集合
  // depsMap<key, dep>,key为tarfet属性
  let depsMap = targetMap.get(target)
  // 如果不存在则重新创建
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取该key对应的依赖集合
  let dep = depsMap.get(key)
  // 如果不存在则重新创建
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 判断是否存在该effect观察者
  if (!dep.has(activeEffect)) {
    // 对应属性值的dep添加该effect
    dep.add(activeEffect)
    // 并且该effect中的deps也添加被观察的属性值的dep
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      // 触发onTrack函数
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

主要是收集目标targetEffect,并且该targetEffectdeps也添加被观察属性值对应的dep,做双映射

trigget函数

作用:通知对应依赖执行回调函数

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 获取与target有关的所有观察者
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }
  // 添加一个依赖集合变量,Set确保唯一性
  const effects = new Set<ReactiveEffect>()
  // 从名字看出来添加函数
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        // 避免循环依赖
        if (effect !== activeEffect) {
          effects.add(effect)
        }
      })
    }
  }
  // clear指的是target清除所有属性触发的事件
  // 类似target = null
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    // 遍历所有属性的观察集合,添加到effects中
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // 监控数组中length的长度变化,pop、shift、push、unshift等都会造成length变化
    depsMap.forEach((dep, key) => {
	  // 将属性length与key大于等于length的属性,全部添加到effects中
	  // 小于length的key自动进行依赖收集了,在这不需要收集
	  // 这里主要针对当直接对Array.length进行赋值时,要收集大于等于length的所有属性的依赖
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
	// 针对对应属性的dep进行添加activeEffect
    if (key !== void 0) {
      add(depsMap.get(key))
    }
	// ITERATE_KEY的收集是以下函数,主要是proxy中handler的第五个参数,监控对象属性集合的获取,例如Object.keys、for(key in target)等
	// function ownKeys(target: object): (string | number | symbol)[] {
  	// 	track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  	// 	return Reflect.ownKeys(target)
	// }
    // also run for iteration key on ADD | DELETE | Map.SET
	// 删除添加都会改变对象与数组的keys长度与length
	// 举个例子,使用watch深层次监控target时,会进行key in target进行递归监控,因此会触发ownKeys,当target进行删除与添加属性时,同样会进行监控到
	// 可能会有其它作用,待使用后继续补充
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
	// 如果存在scheduler,则运行scheduler调度器
	// 否则直接执行effect
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}

总结

effect相当于watchtarget的属性与effect有一个映射表KeyToDepMap target的属性值获取会触发track,此时会收集activeEffect target的属性值修改会触发trigger,此时利用targettargetMap中获取与该对象有关的KeyToDepMap,然后使用属性keyKeyToDepMap获取dep,遍历dep进行添加到执行队列中