Skip to content

第3.3节:依赖收集与派发更新:track 和 trigger 的工作机制

在Vue.js 3的响应式系统中,依赖收集(track)和派发更新(trigger)是整个响应式机制的核心。本节将深入分析这两个关键函数的实现原理,以及支撑它们的数据结构和调度系统。

3.3.1 响应式系统的整体架构

核心组件关系

Vue 3的响应式系统由以下核心组件构成:

typescript
// 全局依赖图:target -> key -> dep
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
type KeyToDepMap = Map<any, Dep>

// 全局版本号,用于优化计算属性
export let globalVersion = 0

// 当前活跃的副作用
export let activeSub: Subscriber | undefined

这个三层嵌套的数据结构形成了完整的依赖图:

  • WeakMap: 以响应式对象为键,避免内存泄漏
  • Map: 以属性键为键,存储该属性的依赖
  • Dep: 依赖对象,管理订阅该属性的所有副作用

3.3.2 ReactiveEffect类:副作用的封装

核心属性与状态管理

typescript
export class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectRunner {
  deps?: Link = undefined
  depsTail?: Link = undefined
  flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
  
  // 调度器:控制副作用的执行时机
  scheduler?: EffectScheduler
  
  constructor(
    public fn: () => T,
    public trigger: () => void,
    public scheduler?: EffectScheduler,
    scope?: EffectScope,
  ) {
    // 初始化逻辑
  }
}

副作用的执行机制

typescript
run(): T {
  this.flags |= EffectFlags.RUNNING
  
  // 清理旧依赖,准备收集新依赖
  prepareDeps(this)
  
  const prevSub = activeSub
  const prevShouldTrack = shouldTrack
  activeSub = this
  shouldTrack = true
  
  try {
    // 执行副作用函数,期间会触发依赖收集
    return this.fn()
  } finally {
    // 恢复执行环境
    activeSub = prevSub
    shouldTrack = prevShouldTrack
    
    // 清理未使用的依赖
    cleanupDeps(this)
    this.flags &= ~EffectFlags.RUNNING
  }
}

3.3.3 依赖收集:track函数的实现

依赖收集的触发时机

当访问响应式对象的属性时,会在getter中调用track函数:

typescript
export function track(target: object, type: TrackOpTypes, key: unknown): void {
  // 只有在应该追踪且有活跃副作用时才收集依赖
  if (shouldTrack && activeSub) {
    // 获取或创建target的依赖映射
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    
    // 获取或创建key的依赖对象
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Dep()))
      dep.map = depsMap
      dep.key = key
    }
    
    // 建立依赖关系
    dep.track()
  }
}

Dep类的track方法

typescript
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
  if (!activeSub || !shouldTrack || activeSub === this.computed) {
    return
  }

  let link = this.activeLink
  if (link === undefined || link.sub !== activeSub) {
    // 创建新的链接
    link = this.activeLink = new Link(activeSub, this)

    // 将链接添加到副作用的依赖列表(作为尾部)
    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) {
    // 重用上次运行的链接,同步版本号
    link.version = this.version
    
    // 将链接移动到尾部,确保依赖列表按访问顺序排列
    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

      // 如果这是头部,更新头部指针
      if (activeSub.deps === link) {
        activeSub.deps = next
      }
    }
  }

  return link
}

操作类型定义

typescript
export enum TrackOpTypes {
  GET = 'get',      // 属性访问
  HAS = 'has',      // in 操作符
  ITERATE = 'iterate', // for...in 循环
}

3.3.4 派发更新:trigger函数的实现

更新触发的时机

当修改响应式对象的属性时,会在setter中调用trigger函数:

typescript
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 从未被追踪过
    globalVersion++
    return
  }

  const run = (dep: Dep | undefined) => {
    if (dep) {
      dep.trigger()
    }
  }

  // 开始批量更新
  startBatch()

  if (type === TriggerOpTypes.CLEAR) {
    // 清空集合,触发所有依赖
    depsMap.forEach(run)
  } else {
    const targetIsArray = isArray(target)
    const isArrayIndex = targetIsArray && isIntegerKey(key)

    if (targetIsArray && key === 'length') {
      // 数组长度变化的特殊处理
      const newLength = Number(newValue)
      depsMap.forEach((dep, key) => {
        if (
          key === 'length' ||
          key === ARRAY_ITERATE_KEY ||
          (!isSymbol(key) && key >= newLength)
        ) {
          run(dep)
        }
      })
    } else {
      // 处理 SET | ADD | DELETE 操作
      if (key !== void 0 || depsMap.has(void 0)) {
        run(depsMap.get(key))
      }

      // 数组索引变化时触发数组迭代依赖
      if (isArrayIndex) {
        run(depsMap.get(ARRAY_ITERATE_KEY))
      }

      // 根据操作类型触发相应的迭代依赖
      switch (type) {
        case TriggerOpTypes.ADD:
          if (!targetIsArray) {
            run(depsMap.get(ITERATE_KEY))
            if (isMap(target)) {
              run(depsMap.get(MAP_KEY_ITERATE_KEY))
            }
          } else if (isArrayIndex) {
            // 新索引添加到数组 -> 长度变化
            run(depsMap.get('length'))
          }
          break
        case TriggerOpTypes.DELETE:
          if (!targetIsArray) {
            run(depsMap.get(ITERATE_KEY))
            if (isMap(target)) {
              run(depsMap.get(MAP_KEY_ITERATE_KEY))
            }
          }
          break
        case TriggerOpTypes.SET:
          if (isMap(target)) {
            run(depsMap.get(ITERATE_KEY))
          }
          break
      }
    }
  }

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

Dep类的trigger和notify方法

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

notify(debugInfo?: DebuggerEventExtraInfo): void {
  startBatch()
  try {
    // 开发模式下按原始顺序调用onTrigger钩子
    if (__DEV__) {
      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()) {
        // 如果notify()返回true,这是一个计算属性
        // 同时通知其依赖
        ;(link.sub as ComputedRefImpl).dep.notify()
      }
    }
  } finally {
    endBatch()
  }
}

操作类型定义

typescript
export enum TriggerOpTypes {
  SET = 'set',      // 设置属性值
  ADD = 'add',      // 添加新属性
  DELETE = 'delete', // 删除属性
  CLEAR = 'clear',   // 清空集合
}

3.3.5 Link类:双向链表的精妙设计

Link类的作用

Link类是连接依赖(Dep)和订阅者(Subscriber)的桥梁,实现了多对多的关系:

typescript
export class Link {
  version: number
  
  // 依赖链表的指针
  nextDep?: Link
  prevDep?: Link
  
  // 订阅者链表的指针
  nextSub?: Link
  prevSub?: Link
  
  prevActiveLink?: Link

  constructor(
    public sub: Subscriber,
    public dep: Dep,
  ) {
    this.version = dep.version
    // 初始化所有指针为undefined
  }
}

双向链表的优势

  1. 高效的插入和删除:O(1)时间复杂度
  2. 内存友好:避免数组的频繁扩容
  3. 顺序保证:维护依赖收集的顺序
  4. 快速遍历:支持正向和反向遍历

3.3.6 调度系统:effect调度器

调度器的作用

调度器(scheduler)控制副作用的执行时机,实现异步更新和批量处理:

typescript
type EffectScheduler = (...args: any[]) => any

// 在ReactiveEffect中使用调度器
notify(): boolean {
  if (this.scheduler) {
    this.scheduler()
    return false
  } else {
    this.trigger()
    return true
  }
}

常见的调度策略

  1. 同步执行:没有调度器时的默认行为
  2. 异步执行:使用Promise.resolve()或nextTick
  3. 批量执行:收集多个更新,一次性执行
  4. 优先级调度:根据优先级决定执行顺序

3.3.7 批量更新机制

批量更新的实现

typescript
let batchDepth = 0
let batchedSub: Subscriber | undefined
let batchedComputed: ComputedRefImpl | undefined

export function startBatch(): void {
  batchDepth++
}

export function endBatch(): void {
  if (--batchDepth > 0) {
    return
  }
  
  // 批量执行副作用
  if (batchedSub) {
    let e: Subscriber | undefined = batchedSub
    batchedSub = undefined
    while (e) {
      const next: Subscriber | undefined = e.next
      e.next = undefined
      e.flags &= ~EffectFlags.NOTIFIED
      if (e.flags & EffectFlags.ACTIVE) {
        e.trigger()
      }
      e = next
    }
  }
  
  // 刷新计算属性
  flushComputedMarkers()
}

批量更新的优势

  1. 性能优化:避免重复执行
  2. 一致性保证:确保状态的一致性
  3. 避免中间状态:用户只看到最终结果

3.3.8 循环依赖检测和处理

循环依赖的检测

typescript
// 在ReactiveEffect.run()中检测循环依赖
run(): T {
  if (this.flags & EffectFlags.RUNNING) {
    // 检测到循环依赖
    if (__DEV__) {
      console.warn('Circular dependency detected')
    }
    return this.fn()
  }
  
  this.flags |= EffectFlags.RUNNING
  try {
    return this.fn()
  } finally {
    this.flags &= ~EffectFlags.RUNNING
  }
}

处理策略

  1. 警告提示:开发模式下给出警告
  2. 中断执行:避免无限递归
  3. 标记检测:使用RUNNING标志检测循环

3.3.9 依赖清理机制

依赖清理的必要性

当副作用重新执行时,需要清理旧的依赖关系,建立新的依赖关系:

typescript
function prepareDeps(sub: Subscriber): void {
  // 将所有依赖的版本标记为-1
  for (let link = sub.deps; link; link = link.nextDep) {
    link.version = -1
    link.prevActiveLink = link.dep.activeLink
    link.dep.activeLink = link
  }
}

function cleanupDeps(sub: Subscriber): void {
  let head
  let tail = sub.depsTail
  let link = tail
  
  // 从尾部开始遍历
  while (link) {
    const prev = link.prevDep
    if (link.version === -1) {
      // 移除未使用的依赖
      removeSub(link)
      removeDep(link)
    } else {
      // 保留使用的依赖
      head = link
    }
    link.dep.activeLink = link.prevActiveLink
    link.prevActiveLink = undefined
    link = prev
  }
  
  // 更新依赖链表的头尾指针
  sub.deps = head
  sub.depsTail = tail
}

3.3.10 性能优化策略

1. 版本号优化

使用全局版本号和依赖版本号避免不必要的计算:

typescript
// 全局版本号,每次响应式变化时递增
export let globalVersion = 0

// 在计算属性中使用版本号优化
get value() {
  if (this.flags & EffectFlags.DIRTY || this.globalVersion !== globalVersion) {
    this.update()
    this.globalVersion = globalVersion
  }
  return this._value
}

2. WeakMap的使用

使用WeakMap存储依赖关系,避免内存泄漏:

typescript
// WeakMap允许对象被垃圾回收
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()

3. 双向链表优化

使用双向链表而非数组,提高插入删除性能:

  • 插入:O(1)
  • 删除:O(1)
  • 遍历:O(n)

3.3.11 实际应用场景

1. 组件更新

typescript
// 组件的render函数作为副作用
const updateComponent = () => {
  const vnode = render() // 触发依赖收集
  patch(oldVnode, vnode) // 更新DOM
}

// 当响应式数据变化时,自动重新渲染
const renderEffect = new ReactiveEffect(updateComponent)

2. 计算属性

typescript
// 计算属性的实现
const computedRef = computed(() => {
  return state.firstName + ' ' + state.lastName // 依赖收集
})

// 当firstName或lastName变化时,自动重新计算

3. 侦听器

typescript
// watch的实现
watch(
  () => state.count, // 依赖收集
  (newVal, oldVal) => {
    console.log(`count changed: ${oldVal} -> ${newVal}`)
  }
)

3.3.12 调试和开发工具

依赖追踪

typescript
// 开发模式下的调试信息
if (__DEV__) {
  dep.track({
    target,
    type,
    key,
  })
}

// onTrack钩子
effect(() => {
  // 副作用逻辑
}, {
  onTrack(e) {
    console.log('依赖收集:', e)
  },
  onTrigger(e) {
    console.log('触发更新:', e)
  }
})

总结

Vue.js 3的依赖收集与派发更新机制是一个精心设计的系统,它通过以下关键技术实现了高效的响应式更新:

  1. 三层依赖图:WeakMap + Map + Dep的数据结构设计
  2. 双向链表:高效的依赖关系管理
  3. 批量更新:避免重复执行,提高性能
  4. 版本号优化:减少不必要的计算
  5. 调度系统:灵活的执行时机控制
  6. 循环依赖检测:保证系统稳定性
  7. 依赖清理:避免内存泄漏

这个系统不仅保证了响应式更新的正确性和高效性,还为开发者提供了强大的调试能力和扩展性。理解这些机制对于深入掌握Vue.js 3的响应式系统至关重要。


微信公众号二维码