响应式依赖收集触发更新过程源码解读

11 阅读12分钟

# const msg = ref('Hello World!')

在源码内部执行过程;

export function ref(value?: unknown) {
  return createRef(value, false)
}


function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T // 存储原始值,用于对比
  public dep?: Dep // 依赖收集容器
  
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = value
    // 如果是深度ref且值是对象,则用reactive包装
    this._value = __v_isShallow ? value : toReactive(value)
  }
  
  get value() {
    trackRefValue(this) // 读取时收集依赖
    return this._value
  }
  
  set value(newVal) {
    // 对比原始值,判断是否真的变化了
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      triggerRefValue(this) // 触发依赖更新
    }
  }
}


  • ref的主要作用是生成一个带有value的getter和setter属性的RefImpl实例
  • 当在使用msg的地方才会触发getter函数进行【依赖收集】,在更改msg的值的时候才会触发更新

Dep类


/**
 * @internal
 */
export class Dep {
//依赖点自身的“变更版本号”。每次 `trigger()` 都会 `version++`,用于 computed 的快速判定/缓存失效。
  version = 0
  /**
   * Link between this dep and the current active effect
   *当前正在运行的 effect(`activeSub`)**  与这个 `Dep` 之间的那条 `Link`(缓存用)。同一个 effect 在一次执行里多次访问同一属性时,避免重复创建/重复订阅
   */
  activeLink?: Link = undefined

  /**
   * Doubly linked list representing the subscribing effects (tail)
   订阅者链表的**尾节点**(tail)。每个 `Link` 对应一个订阅者(effect 或 computed)
   */
  subs?: Link = undefined

  /**
   * Doubly linked list representing the subscribing effects (head)
   * DEV only, for invoking onTrigger hooks in correct order
   订阅者链表的**头节点**(head)
   */
  subsHead?: Link

  /**
   * For object property deps cleanup
   这个 `Dep` 属于哪个 `depsMap`(`targetMap.get(target)`)以及对应的属性 `key` 是谁。主要用于后续清理/调试/定位(比如清掉某个对象某个 key 的 dep)
   */
  map?: KeyToDepMap = undefined
  key?: unknown = undefined

  /**
   * Subscriber counter
   订阅者计数器。`addSub()` 时会 `dep.sc++`,用于统计/优化(例如判断是否需要做某些懒订阅逻辑)
   */
  sc: number = 0

  /**
   * @internal
   */
  readonly __v_skip = true
  // TODO isolatedDeclarations ReactiveFlags.SKIP

  constructor(public computed?: ComputedRefImpl | undefined) {
    if (__DEV__) {
      this.subsHead = undefined
    }
  }

  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
  //activeSub当前的活动的effect
    if (!activeSub || !shouldTrack || activeSub === this.computed) {
      return
    }
    
    //这里的activeLink用于优化同一个值在同一个effect里被多次访问;即当同一个值在被重复访问只会创建一个Link,会直接返回。

    let link = this.activeLink
    if (link === undefined || link.sub !== activeSub) {
     // 1. 创建 Link 节点(核心桥梁)
      link = this.activeLink = new Link(activeSub, this)

      // add the link to the activeEffect as a dep (as tail)
      if (!activeSub.deps) {
        activeSub.deps = activeSub.depsTail = link
      } else {
        link.prevDep = activeSub.depsTail
        activeSub.depsTail!.nextDep = link
        activeSub.depsTail = link
      }

      addSub(link)
    } else if (link.version === -1) {
      // reused from last run - already a sub, just sync version
      link.version = this.version

      // If this dep has a next, it means it's not at the tail - move it to the
      // tail. This ensures the effect's dep list is in the order they are
      // accessed during evaluation.
      if (link.nextDep) {
        const next = link.nextDep
        next.prevDep = link.prevDep
        if (link.prevDep) {
          link.prevDep.nextDep = next
        }

        link.prevDep = activeSub.depsTail
        link.nextDep = undefined
        activeSub.depsTail!.nextDep = link
        activeSub.depsTail = link

        // this was the head - point to the new head
        if (activeSub.deps === link) {
          activeSub.deps = next
        }
      }
    }

    if (__DEV__ && activeSub.onTrack) {
      activeSub.onTrack(
        extend(
          {
            effect: activeSub,
          },
          debugInfo,
        ),
      )
    }

    return link
  }

  trigger(debugInfo?: DebuggerEventExtraInfo): void {
    this.version++
    globalVersion++
    this.notify(debugInfo)
  }

  notify(debugInfo?: DebuggerEventExtraInfo): void {
    startBatch()
    try {
      if (__DEV__) {
        // subs are notified and batched in reverse-order and then invoked in
        // original order at the end of the batch, but onTrigger hooks should
        // be invoked in original order here.
        for (let head = this.subsHead; head; head = head.nextSub) {
          if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
            head.sub.onTrigger(
              extend(
                {
                  effect: head.sub,
                },
                debugInfo,
              ),
            )
          }
        }
      }
      for (let link = this.subs; link; link = link.prevSub) {
        if (link.sub.notify()) {
          // if notify() returns `true`, this is a computed. Also call notify
          // on its dep - it's called here instead of inside computed's notify
          // in order to reduce call stack depth.
          ;(link.sub as ComputedRefImpl).dep.notify()
        }
      }
    } finally {
      endBatch()
    }
  }
}

  • track()(依赖收集):

    • 通过 activeLink 判断“当前 effect 是否已经和我建立过 Link”,没有就 new Link(activeSub, this),并把它挂到:

      • effect 的 dep 链表(activeSub.deps/depsTail)上:方便 effect 下次运行前把旧依赖标记/清理。
      • dep 的 subs 链表(subs/subsHead)上:方便将来触发更新时通知订阅者。
    • link.version === -1 分支:说明这个 Link 是“上一次运行留下的旧 Link,被复用”,这里会把它的 version 同步回当前 dep.version,并把它移动到 effect 的 dep 链表尾部,确保依赖顺序与访问顺序一致(便于清理/调试)。

  • trigger() / notify()(派发更新):

    • trigger() 先做 this.version++,再做 globalVersion++,然后 notify()

    • notify() 遍历 subs 链表,逐个 link.sub.notify() 让订阅者进入调度/执行。

    • 如果订阅者是 computed(notify() 返回 true),会额外触发它自己的 dep.notify(),用于把“依赖了该 computed 的外层 effect”也一起通知到(减少调用栈深度的优化点)。

    • 读取响应式属性时:track(target, key) 找到/创建对应 Dep,然后 dep.track() 把“当前 effect”订阅进来(靠 activeLink/subs 等结构)。
    • 修改响应式属性时:trigger(target, key) 找到对应 Depdep.trigger() 增加版本并 notify(),遍历 subs 去通知所有订阅者更新。

总结:

Vue 3 的 Dep 实现体现了精巧而高效的设计:

  1. 本质是增强的 SetDep = Set<ReactiveEffect> & TrackedMarkers

  2. 双向链接:dep ↔ effect 互相引用,便于清理和调试

  3. 两种使用模式

    • ref:每个实例有自己的 dep 属性
    • reactive:全局 targetMap 中懒创建属性级 dep
  4. 性能优化

    • 二进制位标记避免重复收集
    • Set 的 O(1) 操作
    • 懒创建 + WeakMap 自动 GC
  5. 分离关注点:dep 只管理依赖集合,不涉及值比较或副作用调度

Link:依赖链接(Link)的核心数据结构,实现了双向链表来高效管理依赖关系

export class Link {
  /**
   * - Before each effect run, all previous dep links' version are reset to -1
   * - During the run, a link's version is synced with the source dep on access
   * - After the run, links with version -1 (that were never used) are cleaned
   *   up
   */
  version: number//
版本号,用于依赖清理


  /**
   * Pointers for doubly-linked lists
   */
  nextDep?: Link//下一个依赖
  prevDep?: Link//前一个依赖
  nextSub?: Link//下一个订阅者
  prevSub?: Link//前一个订阅者
  prevActiveLink?: Link 前一个活跃链接

  constructor(
    public sub: Subscriber,
    public dep: Dep,
  ) {
    this.version = dep.version
    this.nextDep =
      this.prevDep =
      this.nextSub =
      this.prevSub =
      this.prevActiveLink =
        undefined
  }
}

五个指针的作用

指针方向用途
nextDep / prevDep水平方向连接同一个 effect 的所有依赖
nextSub / prevSub垂直方向连接同一个依赖 的所有订阅者
prevActiveLink特殊用于 effect 执行期间的上下文管理

Link 在响应式系统中的实际应用

 双向链表的优势在 Vue 中的体现

1. O(1) 的清理操作

typescript

// 清理 effect 的所有依赖
function cleanupEffect(effect: ReactiveEffect) {
  let link = effect.deps  // 从依赖链表头部开始
  
  while (link) {
    // 从 dep 的订阅链表中移除这个 link
    removeSubFromDep(link)  // O(1) 操作!
    
    link = link.nextDep  // 继续下一个依赖
  }
  
  // 清空 effect 的依赖链表
  effect.deps = effect.depsTail = undefined
}

2. 保持访问顺序

typescript

effect(() => {
  console.log(a.value)  // 先访问
  console.log(b.value)  // 后访问
  console.log(c.value)  // 最后访问
})

// effect.deps 链表顺序:a → b → c
// 这个顺序在调试和优化时非常有用

3. 高效的重复访问处理

typescript

effect(() => {
  // 同一个 effect 内多次访问同一个 dep
  const x = a.value * 2
  const y = a.value * 3  // 第二次访问
  
  // Link 被重用,只更新 version
  // 不会创建新的 Link 节点
})

4. 计算属性的级联更新

typescript

const a = ref(1)
const computedA = computed(() => a.value * 2)
const computedB = computed(() => computedA.value * 3)

effect(() => {
  console.log(computedB.value)
})

// 更新链:a → computedA → computedB → effect
// 双向链表使得这个级联通知更高效

⚡ 性能对比:链表 vs Set

操作Set 实现双向链表实现性能提升原因
添加订阅Set.add(effect)链表尾部插入相当
移除订阅Set.delete(effect)链表节点移除链表直接操作指针更快
清理 effectO(m×n) 嵌套循环O(n)  单次遍历链表双向链接直接访问
内存占用每个 Set 额外开销每个 Link 固定大小链表更紧凑
GC 压力频繁创建 SetLink 对象重用减少内存分配

# 高效重复访问机制详解

 什么是"重复访问"?

基本场景

typescript

const count = ref(0)

effect(() => {
  // 同一个 effect 函数内多次访问 count.value
  const double = count.value * 2      // 第一次访问
  const triple = count.value * 3      // 第二次访问(重复访问!)
  const quadruple = count.value * 4   // 第三次访问(再次重复!)
  
  console.log(double, triple, quadruple)
})

更常见的实际场景

typescript

effect(() => {
  // 在条件判断、循环中重复访问
  if (user.value.age > 18) {
    console.log('Adult:', user.value.name)  // 第1次
  }
  
  for (let i = 0; i < user.value.friends.length; i++) {  // 第2次
    console.log('Friend:', user.value.friends[i].name)   // 循环中多次访问
  }
  
  return user.value.score * 1.1  // 第N次
})

⚡ 性能问题:重复访问的代价

Vue 3.3(Set 实现)的问题

typescript

// 伪代码:每次访问都执行完整的依赖收集
function trackRefValue(ref) {
  if (!activeEffect) return
  
  let dep = ref.dep
  if (!dep) {
    dep = ref.dep = new Set()
  }
  
  dep.add(activeEffect)           // ⚠️ 每次都要添加!
  activeEffect.deps.push(dep)     // ⚠️ 每次都要添加!
}

// 结果:一个 effect 对同一个 ref 产生多个相同的依赖记录
// dep = Set { effect, effect, effect, ... }  // 重复!
// effect.deps = [dep, dep, dep, ...]         // 重复!

问题

  1. Set 中重复存储:同一个 effect 被多次添加
  2. 数组重复引用:effect.deps 中有多个相同的 dep 引用
  3. 清理时重复工作:需要多次从同一个 dep 中移除同一个 effect
  4. 触发时重复执行:同一个 effect 可能被触发多次

🚀 Vue 3.4+ 的优化方案

核心机制:Link 节点的重用版本号标记

1. activeLink 快速查找机制

class Dep { // 🔑 关键:记录最后一个链接到当前 effect 的 Link activeLink?: Link = undefined

track() { if (!activeEffect) return

// 1. 先检查是否有可重用的 Link
let link = this.activeLink

// 2. 检查是否可重用:同一个 effect 且未失效
if (link === undefined || link.sub !== activeEffect) {
  // 需要创建新 Link
  link = this.activeLink = new Link(activeEffect, this)
  // ... 建立双向链接
} else if (link.version === -1) {
  // 🔥 可重用:更新版本号,移动节点到尾部
  link.version = this.version
  
  // 移动到 effect 依赖链表的尾部(保持访问顺序)
  if (link.nextDep) {
    // 链表重排操作...
  }
}
// else: link.version === this.version,说明已是最新,什么都不做!

return link 
}}
2. version 版本号系统

typescript

class Dep {
  version = 0  // dep 的版本号,每次 trigger 时递增
  
  trigger() {
    this.version++      // 🔥 值变化时版本号+1
    globalVersion++
    this.notify()
  }
}

class Link {
  version = -1  // 初始值 -1 表示"未收集"或"需要更新"
  
  // 在 track() 中:
  if (link.version === -1) {
    // 需要更新:将 link.version 设置为当前 dep.version
    link.version = dep.version
  } else if (link.version === dep.version) {
    // 已是最新:什么都不做!
    return link
  }
}

调试技巧

一般需要查看收集依赖dep.track,断点设置在let link = this.activeLink;然后按照以下步骤一步一步分析,就可以知道现在正在运行的是哪个Effect,哪个dep桶;

0)先用一句话建立“角色表”

  • Dep(this :代表“某个响应式属性的依赖桶”(比如 state.count 这一个属性就对应一个 Dep)。
  • ReactiveEffect(activeSub :代表“当前正在运行、正在收集依赖的函数”(比如组件 render、watchEffect、computed)。
  • Link:代表“一根线”,把 某个 Dep 和 某个 Effect 连起来(既挂在 Dep 的订阅者链表里,也挂在 Effect 的依赖链表里)。

所以 this.activeLink 的意思是:

“这个 Dep 在‘当前正在运行的那个 effect’上下文里,用来复用的那根 Link”。

上面直接用大白话说就是:每次effect(比如一个watch或一个组件render)去读一个响应式属性时,Vue会在这个属性Dep和当前effect之间“拉一条线”,这条线就是Link。 每次同一个effect再次都这个属性(比如再次渲染),就不用再建新线,Vue会直接服用之前已经存在的那条Link,你看到的 this.activeLink就是“当前这个Dep准备服用给正在跑的effect的那条线。”

假设你把 state.count 想象成一个“广播台”,Dep 是广播台的名单簿,ReactiveEffect 是正在听广播的耳朵(谁在读这个值)。

  • 第一次某个 effect 读 state.count:Vue 给这对“广播台+某个 effect”配了一张票,叫 Link,然后记下来。
  • 下一次同一个 effect 再读 state.count:Vue 发现“这对广播台和 effect 原来就有票”,就直接拿出来继续用(这就是 this.activeLink 里拿出来的那根线)。

所以 this.activeLink 要么是 还没分发给当前 effect 的空位(需要新建一条),要么就是 之前给这个 effect 的票(可以复用)


1)你在 Dep.track 里,先看清楚 3 个东西(照抄操作)

断在这行时:

let link = this.activeLink

第一步:看 activeSub 是谁(当前 effect)

在调试器里展开 activeSub(来自 effect.ts 的全局变量):

  • 看 activeSub.fn:这是它执行的函数(能看出来是 watchEffect / watch / render)
  • 看 activeSub.scheduler:watch 一般长得像 () => scheduler(job, false);组件 render 一般是 queueJob(...) 那种

结论activeSub 就是“当前是谁在读这个响应式值”。


第二步:看 this 是谁(当前 dep 是哪个属性的桶)

展开 this(Dep 实例):

  • 看 this.map 和 this.key

    • this.key:就是属性名(比如 'count'
    • this.map:是那个对象对应的 depsMaptargetMap.get(target) 里的 map)
  • (如果你是从 export function track(target,type,key) 进来的,也可以直接看参数 target 和 key

结论this 就是“当前读到的那个属性(key)对应的 dep 桶”。


第三步:看 this.activeLink 到底是不是“本 effect 的 link”

你现在困惑的“到底是哪个 Link”,用下面这个判断一句话解决:

如果 this.activeLink?.sub === activeSub,那它就是“当前这个 effect ↔ 当前这个 dep”的 link。
如果不是,就说明 this.activeLink 是别的 effect 上次留下的,当前要新建一根 link。

所以你在断点里直接看:

  • this.activeLink 是否为 undefined

  • 如果不为 undefined

    • this.activeLink.sub === activeSub 吗?

2)结合源码:这段代码到底在干嘛(你就按分支理解)

let link = this.activeLink
if (link === undefined || link.sub !== activeSub) {
  link = this.activeLink = new Link(activeSub, this)
  ...把 link 挂到 activeSub.deps 链表尾部...
  addSub(link) // 把 link 挂到 dep.subs 链表尾部
} else if (link.version === -1) {
  ...复用旧 link,并把它挪到 activeSub.depsTail...
}

分支 A:link === undefined || link.sub !== activeSub

意思是:这个 dep 目前没有“属于当前 effect”的 link
所以要做 3 件事:

  1. new Link(activeSub, this):新建一根线(sub=当前 effect,dep=当前 dep)
  2. 把这根线加到 effect 的 deps 链表activeSub.deps/depsTail
  3. 把这根线加到 dep 的 subs 链表addSub(link) 里维护 dep.subs

你就把它理解成: “第一次建立订阅关系”

分支 B:else if (link.version === -1)

意思是:这根 link 以前就存在(已经订阅过),只是这次 run 前被 prepareDeps() 标记成了 -1
现在又读到了同一个 dep,所以:

  • 把 link.version 恢复成 this.version(表示“这次用到了,不要清理”)
  • 如果它在 effect 的 deps 链表中不是尾巴,把它挪到尾巴(维护访问顺序)

你就把它理解成: “同一个 effect 再次运行时复用旧订阅”


3)为什么会有“好几个 Link”,看起来分不清?

因为:

  • 一个 effect 会读很多响应式属性 → 所以 effect.deps 链表里有很多 Link
  • 一个 dep 可能被很多 effect 使用 → 所以 dep.subs 链表里也有很多 Link

但每一根 Link 都有两个指针:

  • link.sub 指向哪个 effect
  • link.dep 指向哪个 dep

所以你在断点里要区分“这根 link 是谁”,就看:

  • link.sub === activeSub 吗?
  • link.dep === this 吗?

只要这两个都对上,它就是“你现在关心的那根”。


4)给你一个“断点观察小口诀”(照着做就不会乱)

你停在 Dep.track() 时,按顺序看:

  1. 我是谁在跑?  → activeSub
  2. 我读的是哪个属性?  → this.key(或上层 track(..., key) 里的 key)
  3. 这根线连的是谁和谁?  → link.sub / link.dep