目录
前言
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 类实现
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 同时维护两个双向链表:
- 订阅者的依赖列表(通过 nextDep/prevDep)
- 依赖项的订阅者列表(通过 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 响应式系统的工作流程可以分为两个阶段:
依赖收集阶段
- 当访问响应式数据时,触发 track 函数
- track 函数通过 targetMap 找到或创建对应的 Dep 实例
- 调用 Dep 实例的 track 方法建立依赖关系
更新触发阶段
- 当修改响应式数据时,触发 trigger 函数
- trigger 函数通过 targetMap 找到对应的 Dep 实例
- 调用 Dep 实例的 trigger 方法触发更新
- trigger 方法递增版本号并调用 notify 通知所有订阅者
性能优化策略
Vue 3 响应式系统采用了多种优化策略来提高性能:
1. 版本号跟踪机制
通过 version 和 globalVersion 实现计算属性的缓存优化,避免无意义的重新计算。
2. 批处理更新
使用 startBatch/endBatch 机制批量处理更新,减少不必要的重复执行。
3. 精确依赖追踪
通过 targetMap 精确建立对象、属性和副作用函数之间的映射关系,确保只触发相关依赖的更新。
4. 链表结构优化
使用双向链表维护依赖关系,提高添加、删除和重新排序的效率。
总结
Vue 3 的 dep.ts 模块是响应式系统的核心,它通过巧妙的数据结构设计和算法优化,实现了高效的依赖收集和更新触发机制。
关键要点:
- 通过 targetMap 建立响应式对象属性与依赖项的映射关系
- 使用 Link 双向链表维护依赖项和订阅者之间的多对多关系
- 通过版本号机制优化计算属性的性能
- 采用批处理机制提高更新效率
- 精确识别不同操作类型,只触发相关依赖的更新
理解 dep.ts 的实现原理,有助于我们更好地使用 Vue 3,也能在遇到性能问题时提供优化思路。