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,也能在遇到性能问题时提供优化思路。

相关推荐
bitbitDown40 分钟前
重构缓存时踩的坑:注释了三行没用的代码却导致白屏
前端·javascript·vue.js
Solon阿杰1 小时前
前端(react/vue)实现全景图片(360°)查看器
javascript·vue.js
前端程序猿i2 小时前
用本地代理 + ZIP 打包 + Excel 命名,优雅批量下载跨域 PDF
前端·javascript·vue.js·html
Danny_FD2 小时前
Vue2 中使用vue-markdown实现编辑器
前端·javascript·vue.js
前端赵哈哈2 小时前
Vue 3 + TypeScript 项目模板
前端·vue.js·vite
一只大黑洋2 小时前
Clipboard.js 复制内容
前端·javascript·vue.js
鹏多多2 小时前
vue混入mixins详解和生命周期影响
前端·javascript·vue.js
岁月宁静3 小时前
MCP 协议应用场景 —— Cursor 连接 Master Go AI
前端·vue.js·人工智能
前端小巷子4 小时前
Vue 3全面提速剖析
前端·vue.js·面试