effect 源码学习记录

602 阅读5分钟

写在前面

effect 可以说是 vue3.0 依赖收集和触发的关键,众多 api 都或多或少的使用到了这个 api。故此,对它的源码进行了一些阅读,本文为个人学习记录,有错误的地方希望各位大神指出。

effect 源码

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) { // 是 effect 的 runner 的话就直接给重载到 fn.raw ,然后包装返回
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) { // 不是懒加载的话就直接执行 effect,是的话需要自己手动执行一次,进行依赖收集
    effect()
  }
  return effect
}

源码使用 ts 写的,所以有一些类型声明,下面是去掉类型声明的代码:

export function effect(
  fn,
  options
) {
  if (isEffect(fn)) { // 是 effect 的 runner 的话就直接给重载到 fn.raw ,然后包装返回
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) { // 不是懒加载的话就直接执行 effect,是的话需要自己手动执行一次,进行依赖收集
    effect()
  }
  return effect
}

流程解析

  1. 先判断传入的 fn 是否已经是 effect 函数(调用 isEffect 进行判断)。如果是,则重载 fn 的值为 fn.raw (raw 属性保存了原始的 fn
  2. 调用 createReactiveEffect 根据配置去生成最终的 effect
  3. options.lazy(如果此配置项为 true,则需要自己手动调用去收集依赖) 不存在则执行 effect,主要用来收集依赖,可以在对应依赖改变时触发执行 effect
  4. 将生成的 effect 返回

流程图如下

effect 流程图

上面的步骤其实逻辑很少,真正的逻辑都是在 createReactiveEffect 之中完成的,下面开始看这个函数的实现

createReactiveEffect 实现

**注:**以后的代码会直接将类型声明去掉

function createReactiveEffect(
  fn,
  options
) {
  const effect = function reactiveEffect(...args) {
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn(...args) // 如果传入的回调函数有返回值,则将返回值返回出去
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++  // 标记每个 effect 的唯一 id
  effect._isEffect = true  // 标记当前的 effect 的类型,主要用于类型检测 (isEffect)
  effect.active = true // 标记当前 effect 有没有被 stop
  effect.raw = fn  // 记录生成 effect 的源头函数
  effect.deps = [] // 和当前 effect 依赖同一个数据的 effect
  effect.options = options // 传入的配置项
  return effect // 返回生成的 effect
}

流程解析

  1. 创建 effect 函数
  2. effect 挂载各种后续需要的属性

这段代码中的重点是 reactiveEffect,下面对它进行解析

function reactiveEffect(...args) {
    if (!effect.active) {  // 如果 effect 被 stop,此时如果 options.scheduler 存在则返回 undefined 否则执行回调 fn
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) { // 表示当前 effect 没有正在被调用
      cleanup(effect) // 将 effect 从 effect.deps 数组中移除,不然无法触发配置 onTrack 
      try {
        enableTracking() // 表示可以执行 track(进行依赖收集)shouldTrack= true
        effectStack.push(effect) // 将当前执行的 effect 加入正在执行的 effect 栈中
        activeEffect = effect // 将当前执行的 effect 标记为正在执行的 effect 
        return fn(...args) // 如果传入的回调函数有返回值,则将返回值返回出去,执行对应回调
      } finally {
        effectStack.pop() // effect 执行完毕,将其推出栈
        resetTracking() // 重置 shouldTrack,以便下一次 effect 调用
        activeEffect = effectStack[effectStack.length - 1] // 重置当前执行的 effect
      }
    }
  }
  1. 从上面的代码中可以看到,其实整个处理过程比较简单,常规的流程走一遍,做种使注册的回调执行,以来的数据更新。
  2. 其实这其中还涉及到了两个关键函数 tracktigger,前者是在首次执行 effect 时收集依赖,后者是在响应式的值改变时去触发对应数据的 effect
  3. 各种原理就是数据的 get 会触发 track,数据的 set 会触发 tigger 去执行对应数据的 effect,更新依赖的值

track 收集依赖

function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) { // shouldTrack 为true 表示需要追踪    activeEffect 表示当前不是在 effect() 执行环境中
    return
  }
  let depsMap = targetMap.get(target) // 获取 targetMap 所有有依赖 effect 的属性
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key) // 获取依赖当前值的 `effect`
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) { // 注意,这里 activeEffect 可能是一个 effect 或者是 undefined 
    dep.add(activeEffect) // 添加依赖
    activeEffect.deps.push(dep) // 将依赖记录在 effect.deps 中
    if (__DEV__ && activeEffect.options.onTrack) {  
      activeEffect.options.onTrack({ // 触发 options.onTrack
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

流程解析

  1. 此函数的流程也不复杂,首先判断当前环境中的公有变量 shouldTrackactiveEffect 是否允许进行依赖收集。
  2. targetMap(公有变量) 中查找是否有当前target 的记录(这里记录的是 target 每个 key 以来的 effects)。有的话取出,没有就新建
  3. 和查找 target 依赖的过程类似,不同的是这里查找的具体的 target.key 的依赖的 effect
  4. 接下来 dep.has(activeEffect) 就对应了 reactiveEffect 中的 clearup 调用来从 effect.deps 中移除自身的逻辑,这里可以看到,这么做主要是在开发模式下触发 options.onTrack

下面是流程图

track 流程图

tigger

响应式的值改变时执行对应数据记录的 effect

function trigger(
  target,
  type,
  key,
  newValue,
  oldValue,
  oldTarget
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 如果 target 没有被记录过,直接返回
    return
  }

  const effects = new Set()  // 保存需要执行的 effect
  const add = (effectsToAdd) => {
    if (effectsToAdd) { // 执行的时候需要判断这个 effect 是否存在(还记得 tarck 中可能保存的 effect 是 undefined)
      effectsToAdd.forEach(effect => {
        // 当前正在执行的 effect 不添加,防止重复执行
        if (effect !== activeEffect || !shouldTrack) {
          effects.add(effect)
        } else {
          // the effect mutated its own dependency during its execution.
          // this can be caused by operations like foo.value++
          // do not trigger or we end in an infinite loop
        }
      })
    }
  }
  // 执行的是清楚任务,也就是把某个 target 要被删除
  if (type === TriggerOpTypes.CLEAR) {
    // 在某个数据被清除之前执行所有的 effect
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) { // 如果这里是给数组的 length 赋值
    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(
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/17/1735af816e6227d0~tplv-t2oaga2asx-image.image)))
    }
    /**
     * 下面的判断逻辑是当 map set object array 在 add,delete 和 set 属性时,如果有依赖是获取他们的 keys ,length 和 size 时,因为属性值得改变。
     * 这些值也需要作出对应的改变。
     */
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map) // 对 map 数据执行 set 操作
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  const run = (effect) => {
    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)
}

流程分析

  1. tigger 的难点在于当有值是依赖数据的 sizelengthkeys 是的判断可能会比较绕,看的时候对一点耐心。
  2. 下面是 1 中提到部分的流程图
    tigger 流程图