Vue3 Effect源码解析

版本:Vue 3.5.17

1. 核心概念

effect 是 Vue 3 响应式系统的核心部分,主要负责依赖追踪和自动响应。它通过 ReactiveEffect 类来封装副作用逻辑,实现依赖收集和触发更新的功能。并且,computed、watch、组件渲染函数等高级 API 在底层都依赖于 effect 来实现。

2. 基本定义

effect 函数用于创建一个响应式副作用。

ts 复制代码
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = {}
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const _effect = new ReactiveEffect(fn, options.scheduler)
  if (!options.lazy) {
    _effect.run()
  }
  return _effect
}
  • 首先会检查传入的 fn 是否本身就是一个 ReactiveEffect 实例,如果是,则取其 raw 属性(指向原始的用户传入函数)。
  • 然后创建一个 ReactiveEffect 实例,传入用户的副作用函数 fn 以及调度器函数 scheduler(如果有提供的话)。
  • 如果没有设置 lazy 选项(默认为 false),则立即调用 _effect.run() 来执行副作用函数并进行初始的依赖收集。
  • 最后返回创建的 ReactiveEffect 实例。

3. ReactiveEffect

简化代码:

ts 复制代码
export class ReactiveEffect<T = any> {
  active = true;
  deps: Dep[] = [];
  parent: ReactiveEffect | undefined;
  constructor(
    public fn: () => T,
    public scheduler: (() => void) | undefined
  ) {}
  run() {
    if (!this.active) {
      return this.fn()
    }
    try {
      this.parent = activeEffect;
      activeEffect = this;
      cleanupEffect(this);
      return this.fn()
    } finally {
      activeEffect = this.parent;
      this.parent = undefined;
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this);
      this.active = false;
    }
  }
}
  • 属性

    • active:表示该 effect 是否处于活动状态,默认值为 true
    • deps:一个数组,存储了该 effect 所依赖的所有依赖集合(Dep 类型,本质是 Set<ReactiveEffect> )。
    • parent:用于处理嵌套 effect 的情况,指向父级 ReactiveEffect 实例。
  • run 方法

    • 首先检查 active 状态,如果为 false,直接执行用户传入的 fn 函数并返回结果。
    • 然后将当前 activeEffect 赋值给 parent,并把自身设置为新的 activeEffect ,这是依赖收集的关键步骤,让系统知道当前正在执行的是哪个 effect
    • 调用 cleanupEffect 方法清理之前收集的旧依赖,避免无效依赖残留。
    • 执行用户传入的副作用函数 fn ,在执行过程中,如果访问了响应式对象的属性,就会触发依赖收集。
    • 最后在 finally 块中,恢复 activeEffect 为之前保存的父级 effect ,并清空 parent
  • stop 方法 :用于停止该 effect 的响应式行为,先调用 cleanupEffect 清理依赖,然后将 active 设置为 false

4. 依赖收集(track 函数)

当访问响应式对象的属性时,会触发 Proxyget 捕获器,进而调用 track 函数进行依赖收集:

ts 复制代码
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!activeEffect || shouldSkipTrack()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}
  • 首先检查当前是否存在活跃的 activeEffect 以及是否需要跳过依赖收集(比如在一些特定场景下,如 readonly 对象的访问)。
  • 然后通过 targetMap(一个全局的 WeakMap<object, Map<key, Set<ReactiveEffect>>> )获取与目标对象 target 对应的 depsMap 。如果不存在,则创建一个新的 Map 并设置到 targetMap 中。
  • 接着从 depsMap 中获取与属性 key 对应的依赖集合 dep ,如果不存在,同样创建一个新的 Set 并设置到 depsMap 中。
  • 最后检查当前 activeEffect 是否已经存在于 dep 中,如果不存在,则将其添加到 dep 中,并且将 dep 添加到 activeEffect.deps 中,建立双向的依赖关系,方便后续清理。

5. 触发更新(trigger 函数)

当修改响应式对象的属性时,会触发 Proxyset 捕获器,进而调用 trigger 函数来通知相关的 effect 重新执行:

ts 复制代码
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect!== activeEffect || type === TriggerOpTypes.CLEAR) {
          effects.add(effect)
        }
      })
    }
  }
  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    if (key!== void 0) {
      add(depsMap.get(key))
    }
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get('*'))
        } else if (isIntegerKey(key)) {
          add(depsMap.get('length'))
        }
        break
    }
  }
  const run = (effect: ReactiveEffect) => {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
  effects.forEach(run)
}
  • 首先从 targetMap 中获取与目标对象 target 对应的 depsMap ,如果不存在则直接返回。
  • 定义一个 add 函数,用于将需要重新执行的 effect 添加到 effects 集合中,并且会根据一些条件(如当前 activeEffect 是否是正在触发更新的 effect 等)进行过滤。
  • 根据不同的操作类型(type ,如 CLEARADD 、属性设置等),从 depsMap 中获取相关的依赖集合,并调用 add 函数添加到 effects 中。
  • 最后遍历 effects 集合,对于每个 effect ,如果其定义了 scheduler 调度器函数,则调用调度器(如 watch 中会利用调度器实现异步回调等逻辑),否则直接调用 effect.run() 重新执行副作用函数。

6. 调度器(scheduler)

effect 可以接受一个可选的 scheduler 函数作为选项,用于自定义 effect 重新执行的方式。例如,在 watch 中,通过设置 scheduler 可以将回调函数加入微任务队列,实现异步批处理更新,避免不必要的同步更新操作,提升性能。

ts 复制代码
// 示例:watch 中使用 scheduler
watch(source, (newValue, oldValue) => {
  // 业务逻辑
}, {
  scheduler: () => {
    queueJob(() => {
      // 实际执行的回调逻辑
    })
  }
})

7. 嵌套 effect 的处理

Vue 3 支持嵌套的 effect ,通过维护一个 effectStack 栈结构来跟踪当前活跃的 effect 。当进入一个新的 effect 时,将其压入栈中,并将 activeEffect 设置为当前 effect ;当执行完毕后,将其从栈中弹出,并恢复 activeEffect 为之前的值。这样可以确保在嵌套场景下,每个属性访问都能正确关联到当前正在执行的 effect ,避免依赖收集混乱。

8. 清理机制(cleanupEffect 函数)

在每次 effect 执行前,会调用 cleanupEffect 函数来清理之前收集的旧依赖:

ts 复制代码
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0, len = deps.length; i < len; i++) {
      deps[i].delete(effect)
    }
    effect.deps.length = 0
  }
}

该函数遍历 effect.deps 中的所有依赖集合,将当前 effect 从这些集合中删除,然后清空 effect.deps 数组。这样做可以避免在动态分支逻辑(如 v-if 切换导致的条件依赖变化)中,残留无效的依赖关系,防止内存泄漏和多次重复调用。

9. 总结

总之,Vue 3.5.17 中 effect 相关的机制通过 ReactiveEffect 类、依赖收集、触发更新、调度器等一系列设计,构成了一个功能强大且灵活的响应式系统,为 Vue 应用的数据响应式更新提供了坚实的基础 。

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax