Vue3系列之effect和ReactiveEffect track trigger源码解析

发布时间:2022-10-28 09:26

介绍几个API的时候,我们发现里面常出现effecttracktrigger,虽然简单说了下track用于依赖收集,trigger来触发更新。但是毕竟没看到具体实现,心里没底。如今便可以一探究竟。

一、ReactiveEffect

1. 相关的全局变量

之前提到的effect,便是ReactiveEffect的实例。用到了一些重要的全局变量。

  • targetMap:弱映射,以目标对象targetkey,其收集到的依赖集depsMap为值,因此通过目标对象target可以获取到对应的所有依赖;
  • activeEffect:当前活跃的effect,随后会被收集起来;
  • shouldTrack:用作暂停和恢复依赖收集的标志;
  • trackStack:历史shouldTrack的记录栈。

targetMap对比reactive篇章中提到的proxyMap

  • 两者都是弱映射;
  • 都以目标对象targetkey
  • targetMap全局只有一个;而proxyMap有四种,分别对应reactiveshallowReactivereadonlyshallowReadonly
  • 一个target在一种proxyMap中最多只有一个对应的代理proxy,因此proxyMap的值为单个的proxy对象;
  • 一个target可以由很多的依赖dep,因此targetMap的值为数据集Map
const targetMap = new WeakMap<any, KeyToDepMap>()
export let activeEffect: ReactiveEffect | undefined
export let shouldTrack = true
const trackStack: boolean[] = []

以及控制暂停、恢复依赖收集的函数:

// 暂停收集
export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}
// 恢复收集
export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}
// 重置为上一次的状态
export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

2. class 声明

在构造器中初始化fn ( 执行run()的过程中调用 ) 、调度器scheduler,并通过recordEffectScope来记录实例的作用域;声明一些实例属性,以及runstop两个方法:

  • activeboolean类型,表示当前的effect是否起作用;
  • deps:当前effect的依赖;
  • parent:指向上一个活跃的effect,形成链表;
  • computed:可选,在computed函数得到的ComputedRefImpl里的effect具有这个属性;
  • allowRecurse,可选,表示是否允许自调用;
  • deferStop:私有,可选,表示stop()是否延迟执行;
  • onStop:可选,函数,在执行stop()时会调用onStop
  • onTrack
  • onTrigger:这两个listener为调试用,分别在依赖收集和响应式更新时触发;
  • runeffect最核心的方法。
  • stop:调用cleanupEffecteffect停止起作用,如果是stop当前活跃的effect,也就是自己停止自己,则会将deferStop调为true,从而延迟停止的时机;触发onStop;将active调为false
export class ReactiveEffect {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl
  /**
   * @internal
   */
  allowRecurse?: boolean
  /**
   * @internal
   */
  private deferStop?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void
  constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
  ) {
recordEffectScope(this, scope)
  }
  run() {
if (!this.active) {
  return this.fn()
}
// 当前活跃的effect
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
// 如果当前活跃的effect就是这个effect本身,则直接返回
while (parent) {
  if (parent === this) {
return
  }
  parent = parent.parent
}
// 依次活跃的effect形成链表,由parent属性连接
try {
  this.parent = activeEffect
  activeEffect = this
  shouldTrack = true
  trackOpBit = 1 << ++effectTrackDepth
  if (effectTrackDepth <= maxMarkerBits) {
// 遍历 this.deps 将其中的effect设置为已捕获 tracked
initDepMarkers(this)
  } else {
// 层级溢出则清除当前副作用
cleanupEffect(this)
  }
  // 尾调用传入的fn
  return this.fn()
} finally {
  // 因为前面有return,因此当 try 的代码块发生异常时执行
  if (effectTrackDepth <= maxMarkerBits) {
// 该方法遍历 this.deps,将其中过气的effect删除,未捕获的effect加入
// effect 就是其中的 dep
finalizeDepMarkers(this)
  }
  trackOpBit = 1 << --effectTrackDepth
  // 复原一些状态
  activeEffect = this.parent
  shouldTrack = lastShouldTrack
  this.parent = undefined
  // 若设置了延迟停止,则执行stop,进行延迟清理
  if (this.deferStop) {
this.stop()
  }
}
  }
  // 清除副作用
  stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
  this.deferStop = true
} else if (this.active) {
  cleanupEffect(this)
  if (this.onStop) {
this.onStop()
  }
  this.active = false
}
  }
}

3. cleanupEffect

cleanupEffect用于清除副作用。接收一个effect,遍历effect.deps,并逐个删除副作用effect。随后清空effect.deps

function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
for (let i = 0; i < deps.length; i++) {
  deps[i].delete(effect)
}
deps.length = 0
  }
}

二、effect 函数

1. 相关ts类型

effect函数有几个相关的类型:

  • ReactiveEffectOptionseffect函数的入参类型之一;
  • ReactiveEffectRunner:是一个函数,且具有effect属性的类型;
export interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}
export interface ReactiveEffectRunner {
  (): T
  effect: ReactiveEffect
}

2. 函数声明

effect函数有两个入参:

  • fn:是一个函数,经处理后用于创建 ReactiveEffect实例_effect
  • options:可选,用于覆盖_effect上的属性。
export function effect(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 处理fn
  if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 根据 fn 创建一个 _effect 
  const _effect = new ReactiveEffect(fn)
  if (options) {
// 用 options 覆盖 _effect 上的属性
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
  }
  // 没有 lazy , 则 _effect 立即执行一次 run()
  if (!options || !options.lazy) {
_effect.run()
  }
  // runner:拿到 _effect.run 并挂上 effect 属性,包装成 ReactiveEffectRunner 类型
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // effect属性指回 _effect 自身,方便使用 runner 调用 run 和 stop
  runner.effect = _effect
  // 返回 runner
  return runner
}

3. stop函数

stop用于清除effect。入参为ReactiveEffectRunner

export function stop(runner: ReactiveEffectRunner) {
  runner.effect.stop()
}

三、track 依赖收集

1. track

一直在说track进行依赖收集,这里看下它到底怎么做的。

  • 以目标对象targetkeydepsMaptargetMap的值;以targetkeykey,使用createDep()创建依赖dep为值,存放在target对应的depsMap中。
  • 通过trackEffects(dep, eventInfo)来收集副作用。
// 全局变量 targetMap
const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
  targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
  depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
  ? { effect: activeEffect, target, type, key }
  : undefined
trackEffects(dep, eventInfo)
  }
}

2. createDep

使用createDep创建一个新的dep。可以看到,dep是个Set实例,且添加了两个属性:

  • wwasTracked的首字母,表示当前依赖是否被收集;
  • nnewlyTracked的首字母,表示当前依赖是否是新收集的。
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}

3. trackEffects

trackEffects用于收集副作用。主要把当前活跃的activeEffect加入dep,以及在activeEffect.deps中加入该副作用影响到的所有依赖。

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
  dep.n |= trackOpBit // set newly tracked
  shouldTrack = !wasTracked(dep)
}
  } else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
  }
  // 当前依赖 dep 还未被捕获 / 当前依赖 dep 中,还没有当前活跃的副作用时,
  // 将当前活跃的副作用 effect 添加进 dep 里,同时在把 dep 加入受副作用影响的依赖集合 activeEffect.deps 中
  if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
  activeEffect!.onTrack({
effect: activeEffect!,
...debuggerEventExtraInfo!
  })
}
  }
}

4. 小结

用一句比较拗口的话来说,依赖收集就是把当前活跃的副作用activeEffect存入全局变量targetMap中的 ( target 对应的 depsMap) 中 (targetkey)对应的 dep ( 类型为Set) 中,并把这个dep加入到受activeEffect副作用影响的所有依赖activeEffect.deps列表中。

四、trigger

触发更新实际上就是触发副作用,因此这一小节决定以与track相反的顺序来介绍。

1. triggerEffect

triggerEffect触发副作用从而更新。当触发更新的副作用effect允许自调用,且不是当前活跃的副作用时,通过调度器scheduler执行副作用或者直接执行run,是实际上触发更新的地方。

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
  effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 实际触发更新的地方
if (effect.scheduler) {
  effect.scheduler()
} else {
  effect.run()
}
  }
}

2. triggerEffects

接收一个dep和用于调试的额外信息。遍历dep中的effect,逐一使用triggerEffect来执行副作用。源码在这里有点蜜汁操作。

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  // 两者互斥,但是执行的操作相同?而且为什么不写在一个 for...of... 里 ?
  for (const effect of effects) {
if (effect.computed) {
  triggerEffect(effect, debuggerEventExtraInfo)
}
  }
  for (const effect of effects) {
if (!effect.computed) {
  triggerEffect(effect, debuggerEventExtraInfo)
}
  }
}

3. trigger

之前一直说trigger触发更新,其实是现在已经知道了,实际是triggerEffect来执行副作用从而实现更新。

这里是创建一个deps数组,根据targetkey和触发更新的操作类型type等参数,来获取所有的相关dep,放入deps。再取出deps中所有的dep里的所有effect,放入effects列表中,通过triggerEffects(effects)来触发所有的相关副作用,最终实现更新。

需要注意的是对于数组:

  • 修改length属性会导致该数组所有依赖的更新;
  • 修数组新增成员会引起length属性相关的依赖的更新,因为length的值发生了变化。
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
// never been tracked
return
  }
  // 用于聚集所有相关依赖
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
// 调用了Set、Map实例的clear方法,将触发全部相关的副作用
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
// 目标对象是数组,且修改了length属性时,会触发全部相关的副作用
depsMap.forEach((dep, key) => {
  if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
  }
})
  } else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
  deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
  case TriggerOpTypes.ADD:
if (!isArray(target)) {
  deps.push(depsMap.get(ITERATE_KEY))
  if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
  }
} else if (isIntegerKey(key)) {
  // 数组下标成员的更改 会引起 length 属性相关的更新
  // new index added to array -> length changes
  deps.push(depsMap.get('length'))
}
break
  case TriggerOpTypes.DELETE:
if (!isArray(target)) {
  deps.push(depsMap.get(ITERATE_KEY))
  if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
  }
}
break
  case TriggerOpTypes.SET:
if (isMap(target)) {
  deps.push(depsMap.get(ITERATE_KEY))
}
break
}
  }
  const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
  if (deps.length === 1) {
if (deps[0]) {
  if (__DEV__) {
triggerEffects(deps[0], eventInfo)
  } else {
triggerEffects(deps[0])
  }
}
  } else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
  if (dep) {
effects.push(...dep)
  }
}
// 这里triggerEffects接受的参数类型为Set,之前的是数组
if (__DEV__) {
  triggerEffects(createDep(effects), eventInfo)
} else {
  triggerEffects(createDep(effects))
}
  }
}

五、小结

1. 依赖收集

targetMap中有depsMap(以targetkey);depsMap中有许多dep(以targetMapkeykey);简单理解为:在编译时根据targetkey,创建副作用,将activeEffect指向新建的副作用,并存放到相关的依赖dep里的过程就是依赖收集。

2. 触发更新

反过来,触发targetkey相关的dep中所有相关的副作用,通过各个副作用上的effect.scheduler()或者effect.run()来实现更新。

Vue3全局实例上挂载属性方法案例讲解 网站建设

Vue3全局实例上挂载属性方法案例讲解

在大多数开发需求中,我们有时需要将某个数据,或者某个函数方法,挂载到,全局实例身上,以便于,在项目全局的任何位置都能够调用其方法,或读取其数据。 在Vue2 中,我们是在 main.js 中 直...
Vue3异步组件suspense使用详解 网站建设

Vue3异步组件suspense使用详解

vue在解析我们的组件时, 是通过打包成一个 js 文件,当我们的一个组件 引入过多子组件是,页面的首屏加载时间 由最后一个组件决定 优化的一种方式就是采用异步组件 ,先给慢的组件一个提示语或者 骨架...