Vue 3 响应式系统核心:dep.ts 解析

目录

前言

Vue 3 的响应式系统是其核心功能之一,它使得数据变化能够自动驱动视图更新。在响应式系统中,[dep.ts]是实现依赖收集和触发更新的关键模块。本文将分析 [dep.ts]的实现原理,帮助开发者更好地理解 Vue 3 响应式系统的内部机制。

核心概念

在深入源码之前,我们需要先了解几个核心概念:

  • Dep (Dependency): 依赖项,代表一个响应式属性。每个响应式属性都有一个对应的 Dep 实例。
  • Subscriber: 订阅者,通常是副作用函数(effect)或计算属性(computed)。
  • Link: 连接 Dep 和 Subscriber 的桥梁,表示依赖关系。
  • targetMap: 全局的 WeakMap,存储目标对象与依赖项的映射关系。

架构概览

Vue 3 响应式系统的核心架构可以用下图表示:

scss 复制代码
                    +----------------+
                    |   targetMap    |
                    |  (WeakMap)     |
                    +--------+-------+
                             |
         +-------------------+-------------------+
         |                                       |
+--------v--------+                    +---------v-------+
|  KeyToDepMap    |                    |     Dep         |
|  (Map)          |                    |                 |
+--------+--------+                    +---------+-------+
         |                                       |
         |                          +------------+------------+
         |                          |            |            |
+--------v--------+      +----------v--+  +------v-----+  +---v--------+
|      Dep        |      |   Link      |  |   Link     |  |   Link     |
|                 |      |(subscriber1)|  |(subscriber2)|  |(subscriber3)|
+-----------------+      +-------------+  +------------+  +------------+

每个响应式对象的每个属性都对应一个 Dep 实例,当属性被访问时,会建立 Dep 与当前活动副作用函数之间的 Link 连接。

源码详解

全局状态管理

typescript 复制代码
// 全局版本号,每当发生响应式变更时递增
// 用于给计算属性提供快速路径,避免在没有任何变化时重新计算
export let globalVersion = 0

globalVersion 是一个全局计数器,每当发生响应式变更时都会递增。它主要用于计算属性的优化,避免在没有任何变化时重新计算。

Link 类表示依赖项(Dep)和订阅者(Subscriber)之间的连接:

typescript 复制代码
/**
 * 表示依赖项(Dep)和订阅者(Subscriber)之间的连接
 * Deps和订阅者之间是多对多的关系,每个连接由Link实例表示
 * Link同时是两个双向链表中的节点:
 * 1. 订阅者的依赖列表
 * 2. 依赖项的订阅者列表
 */
export class Link {
  /**
   * Link的版本号,用于优化性能:
   * - 在每次副作用函数运行前,所有依赖链接的版本号被重置为-1
   * - 运行期间,访问依赖项时版本号与源依赖项同步
   * - 运行结束后,版本号仍为-1的链接(从未被使用)会被清理
   */
  version: number

  /**
   * 双向链表指针
   * nextDep/prevDep: 订阅者的依赖列表指针
   * nextSub/prevSub: 依赖项的订阅者列表指针
   * prevActiveLink: 前一个活动链接
   */
  nextDep?: Link
  prevDep?: Link
  nextSub?: Link
  prevSub?: Link
  prevActiveLink?: Link

  constructor(
    public sub: Subscriber,  // 订阅者
    public dep: Dep,        // 依赖项
  ) {
    // 初始化版本号为依赖项的版本号
    this.version = dep.version
    // 初始化所有指针为undefined
    this.nextDep =
      this.prevDep =
      this.nextSub =
      this.prevSub =
      this.prevActiveLink =
        undefined
  }
}

Link 同时维护两个双向链表:

  1. 订阅者的依赖列表(通过 nextDep/prevDep)
  2. 依赖项的订阅者列表(通过 nextSub/prevSub)

version 属性用于优化性能,通过版本号跟踪机制实现依赖关系的动态更新。

Dep 类实现

Dep 类表示一个依赖项:

typescript 复制代码
/**
 * Dep类表示一个依赖项
 */
export class Dep {
  version = 0                    // 依赖项的版本号,每次变更时递增
  activeLink?: Link = undefined  // 当前活动副作用函数与该依赖项的连接
  subs?: Link = undefined        // 订阅该依赖项的副作用函数链表(尾部)
  subsHead?: Link                // 订阅该依赖项的副作用函数链表(头部),仅在开发模式下使用
  map?: KeyToDepMap = undefined  // 用于对象属性依赖项的清理
  key?: unknown = undefined      // 依赖项关联的键
  sc: number = 0                 // 订阅者计数器
  readonly __v_skip = true       // 标记该对象应被跳过响应式处理

  /**
   * 构造函数
   * @param computed 可选的计算属性引用
   */
  constructor(public computed?: ComputedRefImpl | undefined) {
    if (__DEV__) {
      this.subsHead = undefined
    }
  }

主要属性说明:

  • version: 依赖项的版本号,每次变更时递增
  • activeLink: 当前活动副作用函数与该依赖项的连接
  • subs: 订阅该依赖项的副作用函数链表(尾部)
  • subsHead: 订阅该依赖项的副作用函数链表(头部),仅在开发模式下使用
  • sc: 订阅者计数器

track 方法

typescript 复制代码
/**
 * 跟踪依赖关系
 * @param debugInfo 调试信息
 * @returns Link实例或undefined
 */
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函数将订阅者添加到依赖项的订阅者列表中
    addSub(link)
  } else if (link.version === -1) {
    // 如果链接版本为-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
      }
    }
  }

  // 在开发模式下,触发onTrack钩子
  if (__DEV__ && activeSub.onTrack) {
    activeSub.onTrack(
      extend(
        {
          effect: activeSub,
        },
        debugInfo,
      ),
    )
  }

  // 返回链接
  return link
}

track 方法用于追踪依赖关系,当访问响应式数据时调用。它确保了依赖关系的正确建立和维护。

trigger 和 notify 方法

typescript 复制代码
/**
 * 触发依赖项更新
 * @param debugInfo 调试信息
 */
trigger(debugInfo?: DebuggerEventExtraInfo): void {
  // 递增依赖项版本号
  this.version++
  // 递增全局版本号
  globalVersion++
  // 调用notify方法通知所有订阅者
  this.notify(debugInfo)
}

/**
 * 通知所有订阅者
 * @param debugInfo 调试信息
 */
notify(debugInfo?: DebuggerEventExtraInfo): void {
  // 开始批处理
  startBatch()
  try {
    if (__DEV__) {
      // 在开发模式下,按原始顺序触发onTrigger钩子
      // 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,
            ),
          )
        }
      }
    }
    // 反向遍历订阅者列表,调用每个订阅者的notify方法
    for (let link = this.subs; link; link = link.prevSub) {
      // 如果订阅者的notify方法返回true(表示是计算属性)
      if (link.sub.notify()) {
        // 也调用其依赖项的notify方法
        // 这里是为了减少调用栈深度
        ;(link.sub as ComputedRefImpl).dep.notify()
      }
    }
  } finally {
    // 结束批处理
    endBatch()
  }
}

trigger 方法在响应式数据发生变化时调用,它递增依赖项和全局版本号,并调用 notify 方法通知所有订阅者。

依赖收集过程

typescript 复制代码
/**
 * 跟踪访问响应式属性
 * 这将检查当前正在运行的副作用函数,并将其记录为依赖项
 * 依赖项记录所有依赖于响应式属性的副作用函数
 *
 * @param target - 持有响应式属性的对象
 * @param type - 定义对响应式属性的访问类型
 * @param key - 要跟踪的响应式属性的标识符
 */
export function track(target: object, type: TrackOpTypes, key: unknown): void {
  // 检查是否应该追踪依赖且存在活动订阅者
  if (shouldTrack && activeSub) {
    // 从targetMap中获取目标对象的依赖映射
    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 Dep()))
      dep.map = depsMap
      dep.key = key
    }
    // 在开发模式下传递调试信息
    if (__DEV__) {
      dep.track({
        target,
        type,
        key,
      })
    } else {
      // 生产环境下直接调用track方法
      dep.track()
    }
  }
}

track 函数是依赖收集的入口,当访问响应式属性时调用。它通过 targetMap 建立目标对象、属性和副作用函数之间的映射关系。

更新触发过程

typescript 复制代码
/**
 * 查找与目标(或特定属性)关联的所有依赖项并触发其中存储的副作用函数
 *
 * @param target - 响应式对象
 * @param type - 定义需要触发副作用函数的操作类型
 * @param key - 可用于定位目标对象中的特定响应式属性
 */
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) {
      // 开发环境下传递详细调试信息
      if (__DEV__) {
        dep.trigger({
          target,
          type,
          key,
          newValue,
          oldValue,
          oldTarget,
        })
      } else {
        // 生产环境下直接触发
        dep.trigger()
      }
    }
  }

  // 开始批处理更新
  startBatch()

  // 如果是CLEAR操作(清空集合),触发目标的所有effects
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(run)
  } else {
    // 判断是否为数组及是否为数组索引
    const targetIsArray = isArray(target)
    const isArrayIndex = targetIsArray && isIntegerKey(key)

    // 如果是数组且修改的是length属性
    if (targetIsArray && key === 'length') {
      const newLength = Number(newValue)
      depsMap.forEach((dep, key) => {
        // 触发length、数组迭代以及所有大于等于新长度的索引对应的依赖项
        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) {
            // 对象的ADD操作触发ITERATE_KEY
            run(depsMap.get(ITERATE_KEY))
            if (isMap(target)) {
              // Map的ADD操作额外触发MAP_KEY_ITERATE_KEY
              run(depsMap.get(MAP_KEY_ITERATE_KEY))
            }
          } else if (isArrayIndex) {
            // 数组新增索引时触发length变化
            run(depsMap.get('length'))
          }
          break
        case TriggerOpTypes.DELETE:
          if (!targetIsArray) {
            // 对象的DELETE操作触发ITERATE_KEY
            run(depsMap.get(ITERATE_KEY))
            if (isMap(target)) {
              // Map的DELETE操作额外触发MAP_KEY_ITERATE_KEY
              run(depsMap.get(MAP_KEY_ITERATE_KEY))
            }
          }
          break
        case TriggerOpTypes.SET:
          if (isMap(target)) {
            // Map的SET操作触发ITERATE_KEY
            run(depsMap.get(ITERATE_KEY))
          }
          break
      }
    }
  }

  // 结束批处理更新
  endBatch()
}

trigger 函数是触发更新的入口,当修改响应式属性时调用。它根据操作类型精确地触发相关依赖项的更新。

工作流程

Vue 3 响应式系统的工作流程可以分为两个阶段:

依赖收集阶段

sequenceDiagram participant R as 响应式数据 participant T as track() participant TM as targetMap participant D as Dep participant L as Link participant S as Subscriber R->>T: 访问属性 T->>TM: 获取依赖映射 T->>D: 创建/获取Dep实例 D->>L: 创建Link连接 L->>S: 建立依赖关系
  1. 当访问响应式数据时,触发 track 函数
  2. track 函数通过 targetMap 找到或创建对应的 Dep 实例
  3. 调用 Dep 实例的 track 方法建立依赖关系

更新触发阶段

sequenceDiagram participant R as 响应式数据 participant T as trigger() participant TM as targetMap participant D as Dep participant S as Subscriber R->>T: 修改属性 T->>TM: 获取依赖映射 T->>D: 获取Dep实例 D->>D: trigger() D->>S: 通知订阅者 S->>S: 执行更新
  1. 当修改响应式数据时,触发 trigger 函数
  2. trigger 函数通过 targetMap 找到对应的 Dep 实例
  3. 调用 Dep 实例的 trigger 方法触发更新
  4. trigger 方法递增版本号并调用 notify 通知所有订阅者

性能优化策略

Vue 3 响应式系统采用了多种优化策略来提高性能:

1. 版本号跟踪机制

通过 version 和 globalVersion 实现计算属性的缓存优化,避免无意义的重新计算。

2. 批处理更新

使用 startBatch/endBatch 机制批量处理更新,减少不必要的重复执行。

3. 精确依赖追踪

通过 targetMap 精确建立对象、属性和副作用函数之间的映射关系,确保只触发相关依赖的更新。

4. 链表结构优化

使用双向链表维护依赖关系,提高添加、删除和重新排序的效率。

总结

Vue 3 的 dep.ts 模块是响应式系统的核心,它通过巧妙的数据结构设计和算法优化,实现了高效的依赖收集和更新触发机制。

关键要点:

  1. 通过 targetMap 建立响应式对象属性与依赖项的映射关系
  2. 使用 Link 双向链表维护依赖项和订阅者之间的多对多关系
  3. 通过版本号机制优化计算属性的性能
  4. 采用批处理机制提高更新效率
  5. 精确识别不同操作类型,只触发相关依赖的更新

理解 dep.ts 的实现原理,有助于我们更好地使用 Vue 3,也能在遇到性能问题时提供优化思路。

相关推荐
前端付豪20 小时前
万事从 todolist 开始
前端·vue.js·前端框架
华仔啊21 小时前
别再纠结Pinia和Vuex了!一篇文章彻底搞懂区别与选择
前端·vue.js
月弦笙音1 天前
【Vue3】Keep-Alive 深度解析
前端·vue.js·源码阅读
咖啡の猫1 天前
Vue 实例生命周期
前端·vue.js·okhttp
JNU freshman1 天前
vue 之 import 的语法
前端·javascript·vue.js
剑亦未配妥1 天前
Vue 2 响应式系统常见问题与解决方案(包含_demo以下划线开头命名的变量导致响应式丢失问题)
前端·javascript·vue.js
爱吃的强哥1 天前
Vue2 封装二维码弹窗组件
javascript·vue.js
凉柚ˇ1 天前
Vue图片压缩方案
前端·javascript·vue.js
优弧1 天前
Vue 和 React 框架对比分析:优缺点与使用场景
vue.js