vue源码讲解之 effect解析 (仅包含在effect中使用reacitve情况)

1. effect是什么

Effect 是 Vue 3 响应式系统的核心 API,作用是创建一个响应式副作用 (Reactive Effect)。它在响应式系统中充当"发动机"的角色:

响应式追踪器:自动追踪函数内部访问的所有响应式依赖

自动执行器:当依赖的响应式数据变化时,自动重新运行函数

依赖管理者:管理副作用函数与响应式数据的绑定关系

2. 用法

Vue 3 的源代码设计将 effect 定位为一个底层内部 API。它被实现于 @vue/reactivity 这个独立的响应式核心包中,但在整合到主 vue 包时,并未将其作为公共 API 对外暴露 。这意味着,如果你在项目中通过 import { effect } from 'vue' 的方式引入,将会得到一个错误,因为 vue 包的默认导出中不包含 effect 函数。

这种设计是 Vue 团队有意为之的,旨在"降低开发者的心智负担"。effect 作为响应式副作用的底层抽象,其直接使用涉及依赖收集、执行调度、清理机制等复杂概念,如果广泛暴露,可能会被误用,导致无限循环(例如在 effect 内部修改其依赖的数据)或性能问题。因此,Vue 提供了更高层、更易用的 API(如 watchwatchEffectcomputed)来封装 effect 的能力,这些才是推荐开发者日常使用的工具。

这部分的用法仅展示与 reactive 使用内部源码的执行,如 refcomputed 等待后续内容解锁(后面解析到 refcomputed 会具体去讲)

复制代码
let dummy = 0
const counter = reactive({ num: 0 })    
effect(() => (dummy = counter.num))

expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)

3. effct的执行流程(核心步骤)

整个 effect 的执行围绕track(依赖收集) 和**trigger(触发更新)**展开:

步骤1:初始化阶段

步骤2:执行期间的依赖收集(Track)

步骤3:更新触发阶段(Trigger)

步骤4:重新执行前的清理

步骤5:执行完成

4. 源码解析

这里以用法中的代码为参照,讲一下代码关键部分和经过部分,其他的内容等待后续解锁(后面再具体去说)

effect 函数内部运行过程

  1. 建新的 ReactiveEffect 实例
  2. 将新创建的 ReactiveEffect 实例手动执行内部的 run 方法进行依赖收集

路径: packages\reactivity\src\effect.ts

复制代码
/**
 * 创建一个响应式副作用函数
 * @param fn - 要执行的响应式函数
 * @param options - 可选配置,包含调度器和停止回调
 * @returns ReactiveEffectRunner,用于手动控制 effect
 */
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions,
): ReactiveEffectRunner<T> {
  // 如果传入的 fn 已经是 ReactiveEffectRunner(通过 effect() 返回的对象)
  // 则提取其内部的 ReactiveEffect 实例的 fn 函数
  // 这允许 effect() 接受另一个 effect 返回的 runner 作为参数
  if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  // 创建新的 ReactiveEffect 实例 (重点)
  const e = new ReactiveEffect(fn)


  // ... 此处省略了一部分代码

  // 立即执行一次 effect,进行依赖收集
  // 如果执行过程中抛出错误,先停止 effect 再抛出
  try {
    e.run() // 手动一次effect,运行 fn 进行依赖收集,让响应式数据与effect绑定 
  } catch (err) {
    e.stop()
    throw err
  }

  // 创建一个 runner 函数,绑定到 ReactiveEffect 的 run 方法
  const runner = e.run.bind(e) as ReactiveEffectRunner

  // 将 ReactiveEffect 实例挂载到 runner 上,方便外部通过 runner.effect 访问
  runner.effect = e

  return runner
}
/**
 * 清理函数
 * 执行所有注册的清理回调,用于:
 * 1. effect 重新执行前清理上一次的资源
 * 2. effect 停止时清理资源
 * 
 * @param sub - 实现了 cleanups 和 cleanupsLength 的 ReactiveNode(通常是 ReactiveEffect)
 */
export function cleanup(
  sub: ReactiveNode & { cleanups: (() => void)[]; cleanupsLength: number },
): void {
  // 获取清理函数的数量
  const l = sub.cleanupsLength
  // 如果有清理函数需要执行
  if (l) {
    // 遍历执行所有清理函数
    for (let i = 0; i < l; i++) {
      sub.cleanups[i]()
    }
    // 重置清理函数数量为 0
    sub.cleanupsLength = 0
  }
}

在看 ReactiveEffect 前, 需要知道 依赖收集是什么

概念: 依赖收集是指 Vue.js 用于跟踪组件与其所依赖的数据之间关系的过程 。当数据被读取时,系统会记录下"谁"正在使用这个数据;当数据被修改时,系统就能精确地通知到所有"依赖"于此数据的部分进行更新。其根本目标是实现数据驱动视图的自动化,让开发者无需手动管理数据与视图的同步,从而提升开发效率和应用的性能。

依赖收集的实现本质上是观察者模式在响应式系统中的具体应用

当用法中的代码进行到这里的时候, 首先调用 ReactiveEffect 构造函数,创建 ReactiveEffect 实例对象,

紧接着会执行 run 方法, 执行到 startTracking 为依赖收集做准备,

后面会手动调用,用户提供的 fn ,在执行 fn 时若访问了响应式对象,紧接着就会触发响应式对象的 get 方法

位置: packages\reactivity\src\effect.ts

复制代码
/**
 * 响应式副作用类
 * 负责执行响应式函数并管理依赖追踪和触发更新
 */
export class ReactiveEffect<T = any>
  implements ReactiveEffectOptions, ReactiveNode
{
  /**
   * 依赖链表头指针 - 指向该 effect 依赖的第一个 dep
   */
  deps: Link | undefined = undefined
  /**
   * 依赖链表尾指针 - 指向该 effect 依赖的最后一个 dep
   */
  depsTail: Link | undefined = undefined

  /**
   * 订阅者链表头指针 - 指向订阅该 effect 的第一个订阅者
   */
  subs: Link | undefined = undefined
  /**
   * 订阅者链表尾指针 - 指向订阅该 effect 的最后一个订阅者
   */
  subsTail: Link | undefined = undefined

  /**
   * 状态标志位
   * 初始值: Watching | Dirty
   * - Watching: 表示正在追踪依赖
   * - Dirty: 表示数据已变化,需要重新执行
   * - STOP: 表示已停止
   * - PAUSED: 表示已暂停
   * - Recursed: 表示正在递归执行
   * - ALLOW_RECURSE: 允许递归
   */
   // 默认标志位为 Watching | Dirty
  flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty

  /**
   * 清理函数数组 - 存储 effect 停止时需要执行的清理回调
   * @internal
   */
  cleanups: (() => void)[] = []
  /**
   * 清理函数数量
   * @internal
   */
  cleanupsLength = 0

  // dev only - 调试选项:追踪时的回调
  onTrack?: (event: DebuggerEvent) => void
  // dev only - 调试选项:触发时的回调
  onTrigger?: (event: DebuggerEvent) => void

  // 用户传入的响应式函数
  // @ts-expect-error
  fn(): T {}

  /**
   * 构造函数
   * @param fn - 可选的响应式函数
   */
  constructor(fn?: () => T) {
    // 如果传入了函数,则保存到 fn 属性
    if (fn !== undefined) {
      this.fn = fn
    }
    
    // 此处省略一部分代码 ...
    

  }

  /**
   * 判断 effect 是否处于活跃状态
   * 通过检查 STOP 标志位来判断
   */
  get active(): boolean {
    return !(this.flags & EffectFlags.STOP)
  }

  /**
   * 暂停 effect - 设置 PAUSED 标志位
   * 暂停后,effect 不会被自动触发更新
   */
  pause(): void {
    this.flags |= EffectFlags.PAUSED
  }

  /**
   * 恢复 effect - 清除 PAUSED 标志位
   * 如果有待处理的更新(Dirty 或 Pending),则立即触发
   */
  resume(): void {
    // 清除 PAUSED 标志位
    const flags = (this.flags &= ~EffectFlags.PAUSED)
    // 如果有未处理的变更,则触发更新
    if (flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) {
      this.notify()
    }
  }

  /**
   * 通知 effect 执行
   * 触发依赖变化后的回调
   */
  notify(): void {
    // 只有在未暂停且数据发生变化时才执行
    if (!(this.flags & EffectFlags.PAUSED) && this.dirty) {
      this.run()
    }
  }

  /**
   * 执行 effect 函数
   * 1. 如果已停止,直接执行 fn(不追踪依赖)
   * 2. 否则清理旧的依赖,追踪新的依赖,执行 fn
   * @returns fn 的返回值
   */
  run(): T {
    // 如果 effect 已停止,以非追踪模式执行
    if (!this.active) { // 这里会触发ReactiveEffect实例active的get方法
      return this.fn()
    }
    // 清理旧的依赖追踪
    cleanup(this)
    // 开始追踪依赖,保存之前的订阅者
    const prevSub = startTracking(this)
    try {
      // 执行用户函数,返回其返回值
      return this.fn()
    } finally {
      // 结束追踪依赖
      endTracking(this, prevSub)
      const flags = this.flags
      // 检查是否需要递归触发
      // 如果同时设置了 Recursed 和 ALLOW_RECURSE 标志
      if (
        (flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
        (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)
      ) {
        // 清除 Recursed 标志,然后触发更新
        this.flags = flags & ~ReactiveFlags.Recursed
        this.notify()
      }
    }
  }

  /**
   * 停止 effect
   * 1. 设置 STOP 标志位
   * 2. 从所有依赖的 dep 中解绑
   * 3. 从所有订阅者中解绑
   * 4. 执行清理函数
   */
  stop(): void {
    // 如果已经停止,则直接返回
    if (!this.active) {
      return
    }
    // 设置 STOP 标志位
    this.flags = EffectFlags.STOP
    // 从所有依赖中解绑
    let dep = this.deps
    while (dep !== undefined) {
      dep = unlink(dep, this)
    }
    // 从所有订阅者中解绑
    const sub = this.subs
    if (sub !== undefined) {
      unlink(sub)
    }
    // 执行清理函数
    cleanup(this)
  }

  /**
   * 检查 effect 是否需要重新执行
   * @returns true 表示数据已变化需要执行,false 表示不需要
   */
  get dirty(): boolean {
    const flags = this.flags
    // 如果已经标记为 Dirty,直接返回 true
    if (flags & ReactiveFlags.Dirty) {
      return true
    }
    // 如果有待处理的变更,检查是否真的需要执行
    if (flags & ReactiveFlags.Pending) {
      if (checkDirty(this.deps!, this)) {
        // 标记为 Dirty 并返回 true
        this.flags = flags | ReactiveFlags.Dirty
        return true
      } else {
        // 清除 Pending 标志
        this.flags = flags & ~ReactiveFlags.Pending
      }
    }
    // 没有变化,返回 false
    return false
  }
}

startTrackingeffect 运行之前的准备工作,为依赖收集做准备,保存之前活跃的 sub ,并将活跃 sub 设置为当前 sub

位置: packages\reactivity\src\system.ts

复制代码
/**
 * 开始依赖追踪
 * 在 effect 执行前调用,初始化追踪状态
 * 
 * 1. 递增全局版本号 - 用于判断依赖链接是否最新
 * 2. 清空依赖链表 - 准备建立新的依赖关系
 * 3. 清除旧的状态标志 - 重置为初始追踪状态
 * 4. 设置当前 effect 为活跃订阅者
 * 
 * @param sub - 正在追踪的 ReactiveNode(通常是 ReactiveEffect)
 * @returns 之前活跃的订阅者(用于在 endTracking 时恢复)
 */
export function startTracking(sub: ReactiveNode): ReactiveNode | undefined {
  // 1. 递增全局版本号
  // 每次开始追踪都使用新的版本号,用于判断链接是否为最新的
  ++globalVersion

  // 2. 清空依赖链表尾部
  // 重新建立依赖关系,从空链表开始
  sub.depsTail = undefined

  // 3. 重置状态标志
  // 清除之前可能存在的状态:
  // - Recursed: 递归标志
  // - Dirty: 脏数据标志
  // - Pending: 待处理标志
  // 然后设置 RecursedCheck 标志,表示正在追踪
  sub.flags =
    (sub.flags &
      ~(ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending)) |
    ReactiveFlags.RecursedCheck

  // 4. 设置当前 effect 为活跃订阅者
  // 并返回之前的活跃订阅者(用于嵌套追踪时恢复)
  return setActiveSub(sub)
}


/**
 * 设置当前活跃的订阅者
 * 这是一个重要的全局状态,用于追踪当前正在执行哪个 effect
 * 
 * 使用 try-finally 确保即使发生异常,也能正确更新 activeSub
 * 
 * @param sub - 要设置为活跃的 ReactiveNode(通常是 ReactiveEffect),传入 undefined 表示清除
 * @returns 之前活跃的订阅者
 * 
 * @example
 * // 在 startTracking 中
 * const prevSub = setActiveSub(effect)  // 设置新的,返回旧的
 * 
 * // 在 endTracking 中  
 * setActiveSub(prevSub)  // 恢复之前的
 */
export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined {
  try {
    // 先返回当前的 activeSub
    return activeSub
  } finally {
    // 然后设置新的 activeSub

    activeSub = sub
  }
}

当运行到 ReactiveEffect.run 内调用 用户 fn 若访问了 reactive 代理后的对象, 就会触发响应式的 get 方法, 在 get 触发 track 进行依赖收集

位置: packages\reactivity\src\reactive.ts

复制代码
class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    // 是否只只读对象
    protected readonly _isReadonly = false,
    // 是否为浅响应式对象
    protected readonly _isShallow = false,
  ) {}

  /**
   * 处理代理的 get 操作
   * @param target 目标对象
   * @param key 访问的属性名
   * @param receiver 目标对象的原型或接收者对象
   * @returns 访问结果
   */
  get(target: Target, key: string | symbol, receiver: object): any {
  
   // 此处省略一堆代码


    // 映射目标对象的属性到代理对象
    const res = Reflect.get(
      target,
      key,
      // if this is a proxy wrapping a ref, return methods using the raw ref
      // as receiver so that we don't have to call `toRaw` on the ref in all
      // its class methods
      receiver,
    )

    if (!isReadonly) {
      // 依赖收集的关键
      track(target, TrackOpTypes.GET, key)  // 重点, 典中典
    }


    // 返回映射后的结果
    return res
  }
}

当触发 track 时, 检查当前是否有活跃的 effect 若存在则需要将 target 响应式目标对象 与 effect 进行关联, 这里有一个 targetMap ,它就是用于保存 targeteffect 映射关系, 以 target 为键, 这样在 trigger 时就可以快速找到触发的 effect ,

要注意的是,在 targetMapeffct 并不是以 value 的形式保存的,结合代码,具体怎样关联需要理解下文中 link 部分的代码, 下图的数据结构,或许可以让你了解里面存了哪些数据

位置: packages\reactivity\src\dep.ts

复制代码
class Dep implements ReactiveNode {
  // 订阅者链表头指针
  // 指向第一个依赖该属性的 Link 节点
  _subs: Link | undefined = undefined
  
  // 订阅者链表尾指针
  // 指向最后一个依赖该属性的 Link 节点
  subsTail: Link | undefined = undefined
  
  // 状态标志位
  flags: ReactiveFlags = ReactiveFlags.None

  /**
   * 构造函数
   * @param map - 所属的 KeyToDepMap(target → key → Dep 的映射)
   * @param key - 该 Dep 对应的属性键
   */
  constructor(
    private map: KeyToDepMap,
    private key: unknown,
  ) {}

  /**
   * 获取订阅者链表
   * 实际上是 getter,返回 _subs
   */
  get subs(): Link | undefined {
    return this._subs
  }

  /**
   * 设置订阅者链表
   * 使用 setter,当没有订阅者时自动从 map 中删除
   */
  set subs(value: Link | undefined) {
    this._subs = value
    // 当没有订阅者时,从 map 中删除该 key
    // 这样可以节省内存
    if (value === undefined) {
      this.map.delete(this.key)
    }
  }
}

type KeyToDepMap = Map<any, Dep>

// 主依赖映射表
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
/**
 * 追踪响应式属性访问
 * 当访问一个响应式属性时,调用此函数建立依赖关系
 * 
 * 核心数据结构:
 * targetMap: WeakMap<object, Map<key, Dep>>
 *   ↓
 * {target: {key: Dep}}
 * 
 * @param target - 响应式目标对象(如 reactive 创建的代理对象)
 * @param type - 访问类型(GET / HAS 等)
 * @param key - 访问的属性键(如 'name')
 */
export function track(target: object, type: TrackOpTypes, key: unknown): void {
  // 1. 检查是否有活跃的 effect(正在执行)
  // 如果没有,说明不是 effect 触发的访问,不需要追踪
  if (activeSub !== undefined) {
    // 2. 获取或创建 target 对应的 depsMap
    // depsMap: Map<key, Dep> - 存储该 target 所有属性的依赖
    let depsMap = targetMap.get(target)
    if (!depsMap) { 
      // 如果不存在,为该 target 创建一个新的 Map
      targetMap.set(target, (depsMap = new Map()))
    }

    // 3. 获取或创建 key 对应的 Dep
    // Dep 存储了所有依赖该属性的 effect
    let dep = depsMap.get(key)
    if (!dep) {
      // 如果不存在,创建一个新的 Dep
      depsMap.set(key, (dep = new Dep(depsMap, key)))
    }

    // 4. 开发环境:调试追踪
    if (__DEV__) {
      onTrack(activeSub!, {
        target,
        type,
        key,
      })
    }

    // 5. 建立 Dep 和 Effect 之间的双向链接
    // 这是最核心的一步:让 dep 知道有哪些 effect 依赖它
    link(dep, activeSub!)
  }
}

link 中将 depeffect 做关联, 重点看情况4 创建新的 Link 节点的过程, 结合代码与下图的数据格式或许可以帮助你理解 depsub 双向链表的结构

位置: packages\reactivity\src\system.ts

复制代码
/**
 * 建立 dep(依赖)和 sub(订阅者)之间的双向链表连接
 * 用于追踪响应式依赖关系:dep 知道哪些 sub 依赖它,sub 知道它依赖哪些 dep
 * 
 * @param dep - 响应式依赖(如 reactive 对象的具体属性)
 * @param sub - 订阅者(如 ReactiveEffect 或 Computed)
 */
export function link(dep: ReactiveNode, sub: ReactiveNode): void {
  // 获取 sub 的依赖链表尾部
  const prevDep = sub.depsTail

  // 情况1: 如果 dep 已经在链表尾部,说明已经是最新的,无需处理
  if (prevDep !== undefined && prevDep.dep === dep) {
    return
  }

  // 获取可能的下一个 dep(从尾部或头部)
  const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps

  // 情况2: 如果 dep 已经在链表中(不在尾部),更新版本号并移到尾部
  if (nextDep !== undefined && nextDep.dep === dep) {
    nextDep.version = globalVersion
    sub.depsTail = nextDep
    return
  }

  // 获取 dep 的订阅者链表尾部
  const prevSub = dep.subsTail

  // 情况3: 如果 sub 已经在 dep 的订阅者链表尾部,说明已连接
  if (
    prevSub !== undefined &&
    prevSub.version === globalVersion &&
    prevSub.sub === sub
  ) {
    return
  }

  // 情况4: 创建新的 Link 节点,同时加入到两个链表
  // 1. 将新节点设为 sub.depsTail 和 dep.subsTail
  const newLink =
    (sub.depsTail =
    dep.subsTail =
      {
        // 版本号,用于判断连接是否最新
        version: globalVersion,
        dep,
        sub,
        // 保存前后的指针,用于双向链表
        prevDep,
        nextDep,
        prevSub,
        nextSub: undefined,
      })

  // 2. 更新 dep 链表的指针
  if (nextDep !== undefined) {
    nextDep.prevDep = newLink
  }
  if (prevDep !== undefined) {
    prevDep.nextDep = newLink
  } else {
    // 如果链表为空,设置头部
    sub.deps = newLink
  }

  // 3. 更新 sub 链表的指针
  if (prevSub !== undefined) {
    prevSub.nextSub = newLink
  } else {
    // 如果链表为空,设置头部
    dep.subs = newLink
  }
}

以上的过程已经执行完我们用法中的 effct(...) 代码, 我们知道了 当使用 effect 的时候会默认执行一次 用户fn 进行依赖收集,

完成了第一次的依赖收集, 下面我们要看一下,当我们对 reactive 代理的对象内进行改变时, 又是如何触发 effect 内的 用户fn

当我们修改 reactive 代理对象的属性时, 会触发 MutableReactiveHandler 内的 set 函数, 我们在讲 reactive 的时候有说过,在 set 方法中会执行 trigger 来触发对应的 effect

位置: packages\reactivity\src\baseHandlers.ts

复制代码
class MutableReactiveHandler extends BaseReactiveHandler {
  // 构造函数
  constructor(isShallow = false) {
    // 调用父类构造函数
    // 初始化是否只只读对象为 false
    super(false, isShallow)
  }
  // 处理代理的 set 操作
  set(
    target: Record<string | symbol, unknown>,
    key: string | symbol,
    value: unknown,
    receiver: object,
  ): boolean {
    let oldValue = target[key] // 获取旧值
    
     // 此处省略了一堆代码 

    // key 是否存在于目标对象中
    const hadKey = isArrayWithIntegerKey
      ? Number(key) < target.length
      : hasOwn(target, key)
    // 调用 Reflect.set 方法设置属性值
    const result = Reflect.set(
      target,
      key,
      value,
      receiver,
    )
    // don't trigger if target is something up in the prototype chain of original
    // 不触发原型链上的对象的更新
    // 只有当 receiver 是 target 本身的代理时才触发
    // (而不是原型链上某个对象的代理)
    if (target === toRaw(receiver)) {
      // 如果 key 之前不存在,说明是新增属性
      if (!hadKey) {
        // 触发 ADD 类型的事件(新增)
        trigger(target, TriggerOpTypes.ADD, key, value)
      } 
      // 如果 key 已存在且值发生变化
      else if (hasChanged(value, oldValue)) {
        // 触发 SET 类型的事件(更新)
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) // 重要!!!
      }
    }

    return result
  }
}

当响应式更新的时候 会触发trigger 方法,来通知所有依赖的effect重新执行, 最后

位置: packages\reactivity\src\dep.ts

复制代码
/**
 * 触发响应式更新
 * 当响应式数据发生变化时,调用此函数通知所有依赖的 effect 重新执行
 *
 * @param target - 响应式目标对象
 * @param type - 触发类型(SET / ADD / DELETE / CLEAR)
 * @param key - 变化的属性键(可选)
 * @param newValue - 新值
 * @param oldValue - 旧值
 * @param oldTarget - 旧的目标对象(用于 Map/Set 操作)
 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void {
  // 1. 从 targetMap 中获取该 target 的依赖映射
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 如果从未追踪过,直接返回
    // (比如直接修改了非响应式对象)
    return
  }

  // 2. 定义执行函数 - 触发单个 Dep 的订阅者
  const run = (dep: ReactiveNode | undefined) => {
    if (dep !== undefined && dep.subs !== undefined) {
      
      // 这里省略了一些代码 ...
      
      // 传播更新给所有订阅者
      propagate(dep.subs)
      // 浅层传播(用于处理嵌套响应式)
      shallowPropagate(dep.subs)

      // 这里省略了一些代码 ...
    }
  }

  // 3. 开始批量更新
  startBatch()

  // 4. 根据触发类型处理
  if (type === TriggerOpTypes.CLEAR) {
    // 清空整个集合(如 reactive Map/Set 被 clear)
    // 触发所有依赖该 target 的 effect
    depsMap.forEach(run)

  } else {
    // 省略了一堆代码 ...
    // 5. 触发特定 key 的依赖
    // 对于 SET | ADD | DELETE 操作
    if (key !== void 0 || depsMap.has(void 0)) {
      run(depsMap.get(key))
    }




    // 省略了一堆代码 ...

  }

  // 8. 结束批量更新
  endBatch()
}

propagate 传播更新,将需要触发的 effect 添加到 notifyBuffer 中, 在刷新通知缓冲区 flush 时会通知 effect

位置: packages\reactivity\src\system.ts

复制代码
// 通知缓冲区
const notifyBuffer: (Effect | undefined)[] = []
let notifyBufferLength = 0
/**
 * 传播更新
 * 遍历并触发所有依赖该属性的 effect(订阅者)
 * 使用深度优先遍历 + 栈处理嵌套的订阅者(如 computed)
 * 
 * @param link - 订阅者链表中的第一个 Link 节点
 */
export function propagate(link: Link): void {
  // 下一个订阅者
  let next = link.nextSub
  // 栈,用于保存分支节点(处理嵌套订阅者)
  let stack: Stack<Link | undefined> | undefined

  // 深度优先遍历标签
  top: do {
    // 获取当前 Link 指向的订阅者(ReactiveEffect 或 Computed)
    const sub = link.sub
    let flags = sub.flags

    // 只处理有效的订阅者:Mutable(可变的,如 computed)| Watching(正在追踪的,如 effect)
    if (flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) {
      
      // 情况1: 没有任何标志位,直接设置为 Pending
      if (
        !(
          flags &
          (ReactiveFlags.RecursedCheck |  // 递归检查
            ReactiveFlags.Recursed |       // 递归中
            ReactiveFlags.Dirty |          // 已脏
            ReactiveFlags.Pending)        // 待处理
        )
      ) {
        sub.flags = flags | ReactiveFlags.Pending
      } 
      // 情况2: 没有 RecursedCheck 和 Recursed,重置标志
      else if (
        !(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))
      ) {
        flags = ReactiveFlags.None
      } 
      // 情况3: 有 Recursed 但没有 RecursedCheck
      else if (!(flags & ReactiveFlags.RecursedCheck)) {
        sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending
      } 
      // 情况4: 有 Dirty 或 Pending,且链接有效
      // 需要标记为递归执行
      else if (
        !(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) &&
        isValidLink(link, sub)
      ) {
        // 标记为递归执行 + 待处理
        sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending
        // 只保留 Mutable 标志
        flags &= ReactiveFlags.Mutable
      } 
      // 情况5: 其他情况,跳过
      else {
        flags = ReactiveFlags.None
      }

      // 如果是 Watching(effect),加入待通知队列
      if (flags & ReactiveFlags.Watching) {
        notifyBuffer[notifyBufferLength++] = sub as Effect
      }

      // 如果是 Mutable(computed),需要继续传播给它的订阅者
      if (flags & ReactiveFlags.Mutable) {
        const subSubs = sub.subs
        if (subSubs !== undefined) {
          // 切换到子订阅者链表
          link = subSubs
          // 如果还有更多订阅者,保存当前分支到栈
          if (subSubs.nextSub !== undefined) {
            stack = { value: next, prev: stack }
            next = link.nextSub
          }
          continue  // 继续处理子订阅者
        }
      }
    }

    // 移动到下一个订阅者
    if ((link = next!) !== undefined) {
      next = link.nextSub
      continue
    }

    // 栈不为空,从栈中恢复分支节点
    while (stack !== undefined) {
      link = stack.value!
      stack = stack.prev
      if (link !== undefined) {
        next = link.nextSub
        continue top  // 跳回循环开始
      }
    }

    // 栈为空,遍历结束
    break
  } while (true)
}

/**
 * 结束批量更新
 * 当 batchDepth 降为 0 且有待通知的 effect 时,触发 flush 执行所有 effect
 * 
 * 工作原理:
 * 1. 减少 batchDepth(批量深度计数器)
 * 2. 如果 batchDepth 变为 0,说明所有批量操作已完成
 * 3. 如果有待通知的 effect(notifyBufferLength > 0),执行 flush
 */
export function endBatch(): void {
  // 1. 减少批量深度
  // 2. 如果批量深度变为 0(所有批量操作完成)且有待通知的 effect
  if (!--batchDepth && notifyBufferLength) {
    // 执行所有待通知的 effect
    flush()
  }
}

/**
 * 刷新通知缓冲区
 * 执行所有待通知的 effect
 * 
 * 从 notifyBuffer 中取出所有 effect 并执行其 notify() 方法
 * 执行完毕后重置缓冲区
 */
export function flush(): void {
  // 遍历通知缓冲区中的所有 effect
  while (notifyIndex < notifyBufferLength) {
    // 取出当前索引的 effect
    const effect = notifyBuffer[notifyIndex]!
    // 清除缓冲区中的该位置(释放内存)
    notifyBuffer[notifyIndex++] = undefined
    // 触发 effect 执行
    effect.notify()
  }
  // 重置索引和长度,准备下次使用
  notifyIndex = 0
  notifyBufferLength = 0
}

触发 effect.notify 时会执行 ReactiveEffect.run() 方法, run 方法会重新执行 用户fn , 重新进行依赖收集

位置: packages\reactivity\src\effect.ts

复制代码
export class ReactiveEffect<T = any>
  implements ReactiveEffectOptions, ReactiveNode
{
  /**
   * 依赖链表头指针 - 指向该 effect 依赖的第一个 dep
   */
  deps: Link | undefined = undefined
  /**
   * 依赖链表尾指针 - 指向该 effect 依赖的最后一个 dep
   */
  depsTail: Link | undefined = undefined

  /**
   * 订阅者链表头指针 - 指向订阅该 effect 的第一个订阅者
   */
  subs: Link | undefined = undefined
  /**
   * 订阅者链表尾指针 - 指向订阅该 effect 的最后一个订阅者
   */
  subsTail: Link | undefined = undefined

  /**
   * 状态标志位
   * 初始值: Watching | Dirty
   * - Watching: 表示正在追踪依赖
   * - Dirty: 表示数据已变化,需要重新执行
   * - STOP: 表示已停止
   * - PAUSED: 表示已暂停
   * - Recursed: 表示正在递归执行
   * - ALLOW_RECURSE: 允许递归
   */
  // 默认标志位为 Watching | Dirty
  flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty

  /**
   * 清理函数数组 - 存储 effect 停止时需要执行的清理回调
   * @internal
   */
  cleanups: (() => void)[] = []
  /**
   * 清理函数数量
   * @internal
   */
  cleanupsLength = 0

  // dev only - 调试选项:追踪时的回调
  onTrack?: (event: DebuggerEvent) => void
  // dev only - 调试选项:触发时的回调
  onTrigger?: (event: DebuggerEvent) => void

  // 用户传入的响应式函数
  // @ts-expect-error
  fn(): T {}

  // 省略一堆...

  /**
   * 通知 effect 执行
   * 触发依赖变化后的回调
   */
  notify(): void {
    // 只有在未暂停且数据发生变化时才执行
    if (!(this.flags & EffectFlags.PAUSED) && this.dirty) {
      this.run()
    }
  }

  /**
   * 执行 effect 函数
   * 1. 如果已停止,直接执行 fn(不追踪依赖)
   * 2. 否则清理旧的依赖,追踪新的依赖,执行 fn
   * @returns fn 的返回值
   */
  run(): T {
    // 如果 effect 已停止,以非追踪模式执行
    if (!this.active) {
      return this.fn()
    }
    // 清理旧的依赖追踪
    cleanup(this)
    // 开始追踪依赖,保存之前的订阅者
    const prevSub = startTracking(this)
    try {
      // 执行用户函数,返回其返回值
      return this.fn()
    } finally {
      // 结束追踪依赖
      endTracking(this, prevSub)
      const flags = this.flags
      // 检查是否需要递归触发
      // 如果同时设置了 Recursed 和 ALLOW_RECURSE 标志
      if (
        (flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
        (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)
      ) {
        // 清除 Recursed 标志,然后触发更新
        this.flags = flags & ~ReactiveFlags.Recursed
        this.notify()
      }
    }
  }

  // 省略一堆

}

5. 运行流程(调试流程)

  1. 编写调试代码, 打断点
  2. 创建 ReactiveEffect 实例
  3. 进入构造函数, 保存 `fn`
  4. 执行 Reactive.run 方法
  5. 开始进行依赖追踪
  6. 更改全局版本号, 请空依赖链表尾部, 重置状态标志
  7. 设置当前活跃的订阅者
  8. 执行 用户fn
  9. fn 中读取了响应式代理的属性,这时触发了响应式的get方法
  10. 触发依赖收集
  11. 建立相应的数据结构,让effect与 响应式关联起来
  12. 建立关联
  13. 结束依赖追踪
  14. 设置 reactive 代理对象的属性值,
  15. 触发 MutableReactiveHandler.set , 执行 trigger 触发 effect 执行
  16. 获取 depsMap
  17. 开始批量更新
  18. 触发执行函数传播事件
  19. 结束批量更新
  20. 通知所有的 effectrun
  21. run
  22. run 时又会触发 get 重新进行依赖收集

6. 问题

6.1 effet 中观察者模式的实现

依赖收集的实现本质上是观察者模式在响应式系统中的具体应用

在这个模式中:

  • 观察目标 :是响应式数据(如 reactive 对象或 ref.value)。
  • 观察者 :是依赖于这些数据的实体,例如组件的渲染函数、computed 计算属性或 watch 侦听器。

其工作流程遵循一个清晰的闭环,可以分为三个核心步骤:

  1. 数据劫持:让数据"可观测" 这是依赖收集的基础。Vue 通过拦截对数据的访问和修改操作,使其变得"可观测"。在 Vue 2 中,这通过 Object.defineProperty 为每个属性定义 gettersetter 实现;而在 Vue 3 中,则采用了更强大的 Proxy 对象来代理整个对象,从而能够监听动态增删属性、数组索引变化等更广泛的操作。
  2. 依赖收集:记录"谁用了我" 当组件渲染或副作用函数执行时,一旦访问了某个响应式属性,就会触发其 getter(或 Proxyget 拦截器)。此时,Vue 会将当前正在执行的副作用函数 (在 Vue 2 中称为 Watcher,在 Vue 3 中称为 ReactiveEffect)记录下来,并将其添加到该属性对应的"依赖列表"中。这个过程就是"收集依赖"。在 Vue 3 的实现中,这个依赖关系通过一个核心的数据结构 targetMap(一个 WeakMap)来存储,其映射关系为:原始对象 -> 属性键 -> 依赖该属性的副作用函数集合
  3. 派发更新:数据变了,通知"订阅者" 当响应式数据的值被修改时,会触发其 setter(或 Proxyset 拦截器)。系统会从之前建立的依赖关系(即 targetMap)中,找到这个属性对应的所有副作用函数集合,然后逐一执行它们(或通过调度器进行异步批量执行),从而触发视图的重新渲染或计算属性的重新计算。这就是"触发更新"或"派发更新"。

6.2 观察者模式 和 订阅发布模式有什么区别

观察者模式与发布-订阅模式是软件设计中两种用于实现对象间通信的经典模式,它们都旨在实现解耦,但在耦合程度、实现机制和适用场景上存在显著差异。根据搜索结果,两者的核心区别在于**是否存在一个独立的"调度中心"或"事件通道"**来中介发布者与订阅者之间的通信。

一、 核心概念与架构差异

  1. 观察者模式:直接依赖的"一对多"通知 观察者模式定义了一种一对多的依赖关系 ,当一个对象(称为"主题"或"被观察者")的状态发生改变时,它会直接通知 所有依赖于它的对象(称为"观察者"),并自动更新它们。在这种模式中,主题和观察者是互相感知、直接关联 的。观察者需要将自己注册到主题的观察者列表中,主题则负责维护这个列表并在状态变化时遍历列表、调用每个观察者的更新方法。其架构是面向目标和观察者编程的。
  2. 发布-订阅模式:通过中介的"消息范式" 发布-订阅模式是一种消息范式 。在这种模式下,消息的发送者(发布者)不会将消息直接发送给特定的接收者(订阅者),而是发布到某个调度中心、事件通道或消息代理 。订阅者向调度中心订阅感兴趣的消息类型,当匹配的消息发布时,由调度中心负责将消息推送给相应的订阅者。因此,发布者和订阅者彼此不知对方的存在,实现了彻底的解耦 。其架构是面向调度中心编程的。

二、 耦合度与通信机制对比

这是两种模式最本质的区别,决定了它们的灵活性和适用性。

对比维度 观察者模式 发布-订阅模式
耦合关系 紧耦合。观察者必须知道主题的存在并主动注册,主题也必须知道所有观察者的接口以进行通知。 松耦合。发布者和订阅者完全解耦,双方只与调度中心交互,无需知道对方的具体实现甚至存在。
通信中介 无中介。主题直接持有观察者引用列表,并负责通知。 有中介。必须存在一个调度中心(事件通道)来管理订阅关系、过滤和分发消息。
通知粒度 广播式通知 。主题状态变化会通知所有已注册的观察者,观察者通常收到相同的更新信息。 筛选式通知。订阅者可以声明只对特定类型或内容的消息感兴趣,调度中心会进行过滤,只有符合条件的订阅者才会收到通知。
相关推荐
英俊潇洒美少年2 小时前
Vue reactive 底层 Proxy 完整流程(依赖收集 + 触发更新)
前端·javascript·vue.js
Qlittleboy2 小时前
<el-form @submit.native.prevent> elementUI的里面的input的元素的回车事件后总是自动提交表单
前端·javascript·elementui
Carsene2 小时前
Docsify 文档缓存问题终极解决方案:拦截请求自动添加版本号
前端·javascript
周淳APP2 小时前
【VDOM,Diff算法,生命周期,并发请求】
前端·javascript·vue.js
Linncharm2 小时前
重写一个「年久失修」的开源项目:把 jQuery + CoffeeScript 的 3D 户型图工具迁移到 TypeScript + Three.js r181
前端
竹林8182 小时前
从“后端验证”到“前端签名”:我在Web3项目中重构用户身份认证的实战记录
前端·javascript
农夫山泉不太甜2 小时前
Expo开发App实战指南:从技术选型到架构设计
前端
进击的尘埃2 小时前
Vite 插件开发入门:从零写一个自动生成路由的插件
javascript
高桥凉介发量惊人2 小时前
状态管理与架构篇-异步状态管理:加载、空态、错误态统一处理
前端