Vue3源码之依赖收集和触发更新

395 阅读4分钟

我们知道vue3提供了响应式函数方法如reactive,ref,我们可以通过reactive或者ref创建一个响应式变量,然后在setup方法内返回变量值,然后我们就可以在template模板中使用该响应式变量;当响应式变量值被更新的时候,对应的模板值会被更新。

那么,vue是怎么在某一个响应式变量值更新的时候,触发页面的更新操作呢?这就是本篇文章带大家继续深入了解的响应式原理部分 依赖收集和通知更新

在上一篇文章中# Vue3源码解析--reactive篇# Vue3源码解析--ref篇我们了解到,当响应式变量值被读取,会进行对应的依赖收集,在响应式变量值被修改时,会进行通知更新操作。

1. 依赖收集

当响应式变量值被获取的时候,会进行对应的依赖收集

// 收集方式类型枚举定义
export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}
get value() {
    // 依赖收集
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
}

track()方法接受三个参数:被收集对象,收集类型,key

// targetMap 依赖管理中心,用于收集依赖和触发依赖
const targetMap = new WeakMap<any, KeyToDepMap>()
// 如果存在多个effect,则依次放入栈中
const effectStack: ReactiveEffect[] = [] 
// 存放当前活动effect副作用函数
let activeEffect: ReactiveEffect | undefined

// track的主要作用是将响应式副作用函数,添加为对应target.key保存的set结构中
// 当被通知更新阶段,再依次取出执行
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 依赖收集进行的前置条件:
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // targetMap大概样子
  // targetMap(weakmap) = {
  //     target1: map{
  //       key1: set(fn1,fn2,fn3...)
  //       key2: set(fn1,fn2,fn3...)
  //     },
  //     target2: map{
  //       key1: set(fn1,fn2,fn3...)
  //       key2: set(fn1,fn2,fn3...)
  //     },
  // }

  // 根据target对象取出当前target对应的depsMap结构
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 第一次收集依赖可能不存在
    targetMap.set(target, (depsMap = new Map()))
  }
  // 根据key取出对应的用于存储依赖的Set集合
  let dep = depsMap.get(key)
  if (!dep) {
    // 第一次可能不存在
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // 将当前的副作用函数收集进依赖中
    dep.add(activeEffect)
    // 并在当前副作用函数的 deps 属性中记录该依赖
    activeEffect.deps.push(dep)
  }
}

2. effect

track进行依赖收集,主要是收集目标target key被读取对应的副作用函数,effect就是一个将fn函数包装为副作用函数的方法。

我们先看下effect相关的类型定义,有助于我们理解effect实现

// 响应式副作用函数的类型定义
export interface ReactiveEffect<T = any> {
  (): T
  _isEffect: true // 属性标识这是一个副作用
  id: number // 序号id,标识effect唯一性
  active: boolean // 标识这个副作用启用和停用的状态
  raw: () => T // 保存初始传入的函数
  deps: Array<Dep> // 这个副作用的所有依赖
  options: ReactiveEffectOptions
  allowRecurse: boolean
}
// effect第二个参数options类型定义
export interface ReactiveEffectOptions {
  lazy?: boolean
  scheduler?: (job: ReactiveEffect) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
  allowRecurse?: boolean
}
// effect
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 如果传入参数已经是effect包装的响应式副作用函数,则获取副作用函数保存的原始值函数
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options) // 返回一个响应式的effect函数
  if (!options.lazy) {
    // 非惰性的,默认会执行一次。
    effect()
  }
  return effect
}
// createReactiveEffect
// 如果存在多个effect,则依次放入栈中
const effectStack: ReactiveEffect[] = [] 
// 存放当前活动effect副作用函数
let activeEffect: ReactiveEffect | undefined
// 序号,每次执行createReactiveEffect执行++1
let uid = 0

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    
    if (!effectStack.includes(effect)) {
      // 保证effect不会被重复保存在effectStack
      cleanup(effect)
      try {
        enableTracking()
        // 在取值之前将当前effect放到栈顶
        effectStack.push(effect)
        // 全局变量保存当前effect
        activeEffect = effect
        // 执行effect的回调就是一个取值的过程
        return fn()
      } finally {
        // 从effectStack栈顶将自己移除
        effectStack.pop()
        resetTracking()
        // 将effectStack的栈顶元素标记为activeEffect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++ // 序号id加1
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true // 标识fn已经是effect副作用函数
  effect.active = true
  effect.raw = fn // 保存原值fn
  effect.deps = [] // 依赖了哪些属性,哪些属性变化了需要执行当前effect
  effect.options = options
  return effect
}

3. trigger

当响应式变量值被修改时候,会通知依赖该变量的依赖着更新,在track阶段,依赖变量key的依赖着也就是副作用函数被依次保存在set结构中,当值被修改时候,则将set保存的副作用函数依次执行,触发vnode patch更新

export function trigger(
  target: object,
  type: TriggerOpTypes, // set add delete clear
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 如果没有收集过依赖,直接返回
    return
  }
  // 存储依赖的effect
  const effects = new Set<ReactiveEffect>() // [fn1,fn2,fn3]
  // 添加存储
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }
  // 在值被清空前,往相应的队列添加 target 所有的依赖
  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // 修改数组length
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // SET | ADD | DELETE
    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
  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()
    }
  }
  // 遍历set结构保存的effect副作用函数
  effects.forEach(run)
}