vue3源码之effect如何创建响应式副作用函数

439 阅读4分钟

说明:我用的这份源码呢是3.0.11,是很早的vue3的版本,后面新的vue源码呢我也看了一些,我们主要会用的源码大多都是小改小动,做了一些更好的封装。所以我认为目前我的这份老源码也是可以读的,哈哈哈哈
源码在packages/reactivity/src/effect.ts

effect作用

effectvue3实现响应式的核心。
effect的执行过程:他会包裹一个函数,让被包裹的函数成为一个待收集的函数,而这个被包裹的函数在执行之时,函数内部的响应式数据触发getter操作,此时就会收集待收集的函数,于是便建立起响应式数据和函数之间的依赖关系,后续响应式数据发生改变便会触发依赖、函数执行。

正文

还是先看源码,下面是定义部分

export interface ReactiveEffectOptions {
  lazy?: boolean//是否延迟触发
  scheduler?: (job: ReactiveEffect) => void//调度函数(这个调度函数有很多都是加入队列,然后异步执行)
  onTrack?: (event: DebuggerEvent) => void//收集依赖时的回调函数(用于调试)
  onTrigger?: (event: DebuggerEvent) => void//触发依赖时的回调函数(用于调试)
  onStop?: () => void//停止时的回调函数
  allowRecurse?: boolean//是否允许递归
}

/*
effect 接收两个参数
fn 回调函数
options 参数
*/
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
 // 如果已经是 effect 先重置为原始对象
    fn = fn.raw
  }
  //创建effect
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
  //配置里没有传入lazy,直接执行一次,执行过程中创建依赖关系,收集依赖关系。     
    effect()
  }
  return effect
}

effcet入口也就那样,好像没什么大不了的,简简单单!下面我们来看effect是如何创建的。

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
  //如果我们调用了effect.stop,那么effect失活
    if (!effect.active) {
    //如果没有调度者,直接返回,否者直接执行并返回结果
      return options.scheduler ? undefined : fn()
    }
    
    //判断effectStack中有没有effect, 如果在则不处理
    if (!effectStack.includes(effect)) {
    //清除effect依赖
      cleanup(effect)
      try {
        //开始收集依赖
        enableTracking()
        //入栈
        effectStack.push(effect)
        //将activeEffect置为effect,方便后续操作进行依赖收集
        activeEffect = effect
        //这里执行了传入函数fn,在执行fn的过程中。里面的响应式数据就会收activeEffect
        return fn()
      } finally {
        //出栈
        effectStack.pop()
        //重置依赖
        resetTracking()
        //重置activeEffect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return 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
  }
}

上面的函数中effectStack存在的意义在于,effect包裹的函数体里面也有effect,这样全局的activeEffect就会丢失。
为什么要cleanup,因为每次effect执行之时,它的依赖也许会发生改变。打个比方:我们都知道渲染函数执行,生成vnode,函数内部响应式数据触发getter,触发依赖收集,假如我使用了v-if切换渲染两个不同的列表,第一次执行effect就依赖第一次列表的数据,第二次执行effect就依赖第二次列表的数据(因为数据在getter被触发时才会收集依赖)。

track收集依赖

响应式数据被访问之时(也就是触发响应式getter操作),track函数会被调用,此时收集依赖。

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 每个target会对应一个depsMap
  let depsMap = targetMap.get(target)
  // 判断是否直接可以找到一个depsMap
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 每个key在depsMap中对应一个dep集合
  let dep = depsMap.get(key)
  // 判断之前是否有这个集合
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // 手机当前激活的effect作为依赖
    dep.add(activeEffect)
    // 将收集的依赖放入到activeEffect的deps中
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {

    // 开发环境会触发onTrack, 仅用于调试
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}

track的代码比较简单,主要的难点就是targetMap(是一个weakMap,保存着每个对象和depsMap的映射关系)这个数据结构,targetMap里面保存着所有的依赖关系。拿对象打比方:每个对象的每个key都有自己的依赖函数(可能是多个依赖函数,如果有多个函数都用到了此数据),那么对象的某个key对应的值发生了改变,我们如何拿到他的依赖函数呢,我们就先从targetMap拿到这个对象的所有依赖depsMap(是一个map,保存着每个key对应的依赖函数),再从depsMap根据key拿到具体的dep(是一个set,因为可能有多个依赖函数,set也避免重复添加)。

trigger触发依赖

我们都知道响应式数据的改变,会触发依赖,下面将详细解析触发过程发生了什么。

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 通过targetMap拿到target对应的依赖集合
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  // 创建一个effects的集合,用来保存即将执行的依赖函数
  const effects = new Set<ReactiveEffect>()

  // 添加effects的函数
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
        //这里是避免造成无限循环,因为如果在执行依赖函数时改变某个响应式数据,
        //又会触发此依赖函数(基本依赖函数不会改变响应式数据,一般都是些渲染函数,
        //渲染函数仅用来生成vnode)
          effects.add(effect)
        }
      })
    }
  }

//从这里开始到run函数之前,都是判断响应式数据改变的方式,然后拿到对应的依赖的函数,添加到effects
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    //set或者map数据的clear,触发set/map的所有依赖
    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) {
    //viod 0 通常就是指的 undefined
    //对象具体的某个key对应的属性发生改变
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    // 判断是ADD,还是DELETE, 还是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
    }
  }

  // 定义执行的函数
  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执行run函数
  effects.forEach(run)
}

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}


个人拙见,如有错误,希望指出,谢谢!