Appearance
第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
}
}双向链表的优势
- 高效的插入和删除:O(1)时间复杂度
- 内存友好:避免数组的频繁扩容
- 顺序保证:维护依赖收集的顺序
- 快速遍历:支持正向和反向遍历
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
}
}常见的调度策略
- 同步执行:没有调度器时的默认行为
- 异步执行:使用Promise.resolve()或nextTick
- 批量执行:收集多个更新,一次性执行
- 优先级调度:根据优先级决定执行顺序
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()
}批量更新的优势
- 性能优化:避免重复执行
- 一致性保证:确保状态的一致性
- 避免中间状态:用户只看到最终结果
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
}
}处理策略
- 警告提示:开发模式下给出警告
- 中断执行:避免无限递归
- 标记检测:使用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的依赖收集与派发更新机制是一个精心设计的系统,它通过以下关键技术实现了高效的响应式更新:
- 三层依赖图:WeakMap + Map + Dep的数据结构设计
- 双向链表:高效的依赖关系管理
- 批量更新:避免重复执行,提高性能
- 版本号优化:减少不必要的计算
- 调度系统:灵活的执行时机控制
- 循环依赖检测:保证系统稳定性
- 依赖清理:避免内存泄漏
这个系统不仅保证了响应式更新的正确性和高效性,还为开发者提供了强大的调试能力和扩展性。理解这些机制对于深入掌握Vue.js 3的响应式系统至关重要。
