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

6 阅读8分钟

# 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
  }
}