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 应用的数据响应式更新提供了坚实的基础 。

相关推荐
爷_2 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
charlee443 小时前
行业思考:不是前端不行,是只会前端不行
前端·ai
Amodoro4 小时前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin4 小时前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说5 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang4535 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2435 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
苹果醋35 小时前
iview中实现点击表格单元格完成编辑和查看(span和input切换)
运维·vue.js·spring boot·nginx·课程设计
武昌库里写JAVA6 小时前
iView Table组件二次封装
vue.js·spring boot·毕业设计·layui·课程设计
三口吃掉你6 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat