前言
你是否曾想过,Vue3 中的 reactive
究竟是如何在幕后管理数据和视图更新的?本文将带你深入挖掘 Vue3 响应式系统的精髓,从源码中解开 reactive
的工作原理,帮助你在实际开发中更好地运用这一强大的功能。
学习 reactive
源码,你可以获得以下技能和知识:
- 深入理解 JavaScript 中的
Proxy
和Reflect
,掌握它们的使用方法及优势。 - 掌握响应式系统的依赖追踪与视图更新,即 发布订阅模式。
- 处理深度嵌套对象,并理解如何支持浅响应和深响应模式。
- 性能优化技巧,如何高效管理依赖和更新。
上期文章讲解了创建 reactive
对象以及修改时用到的方法和类,本期我们将深入 MutableReactiveHandler
类中的一些小方法。
1.追踪到 packages/reactivity/src/dep.ts
文件
该文件包含了 Vue 3 响应式系统的核心部分,主要负责依赖收集与触发机制。它实现了 发布订阅模式,通过 Dep
和 Link
类来管理数据与视图的更新关系。下面逐步讲解文件中的关键部分和它们在发布订阅模式中的作用。
2.Link
类解析
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
this.nextDep =
this.prevDep =
this.nextSub =
this.prevSub =
this.prevActiveLink =
undefined
}
}
Link
类表示一个 订阅者(sub
)与一个 依赖(dep
)之间的关联。version
:记录依赖的版本号,在每次依赖变更时更新,用于确保只在数据发生变化时才触发视图更新。nextDep
和prevDep
:在Dep
类中形成一个 双向链表,用于跟踪订阅者的依赖关系。nextSub
和prevSub
:在订阅者(Effect
)之间形成双向链表,方便管理每个订阅者的依赖关系。
总结:Link
是 Dep
(依赖)和 Effect
(订阅者)之间的 桥梁,它维护着 依赖和订阅者的关系,并通过链表连接。
3.Dep
类解析
export class Dep {
version = 0 // 依赖版本,数据变化时增加
activeLink?: Link = undefined // 当前激活的 Link
subs?: Link = undefined // 订阅者链表
subsHead?: Link // 订阅者链表头部(用于开发调试时)
map?: KeyToDepMap = undefined // 依赖的 map,用于对象属性
key?: unknown = undefined // 当前依赖的 key(属性名)
sc: number = 0 // 订阅者计数
constructor(public computed?: ComputedRefImpl | undefined) {
if (__DEV__) {
this.subsHead = 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)
// 将 link 添加到当前 activeEffect 的依赖链表尾部
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
addSub(link) // 将 link 添加到 dep 的订阅者链表中
} else if (link.version === -1) {
link.version = this.version
// 如果 link 已经存在,调整它在链表中的位置(确保依赖访问顺序)
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
}
}
}
// 在开发模式下,调用 track 钩子
if (__DEV__ && activeSub.onTrack) {
activeSub.onTrack(extend({ effect: activeSub }, debugInfo))
}
return link
}
trigger(debugInfo?: DebuggerEventExtraInfo): void {
this.version++ // 增加版本号
globalVersion++ // 增加全局版本号
this.notify(debugInfo) // 通知所有订阅者
}
notify(debugInfo?: DebuggerEventExtraInfo): void {
startBatch() // 批量更新开始
try {
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()) {
// 如果是计算属性,通知它的依赖更新
(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
endBatch() // 批量更新结束
}
}
}
Dep
代表数据的依赖管理类,维护了对某个响应式数据属性的所有订阅者(Effect
)。track()
:收集依赖,建立Dep
与Effect
之间的关联。trigger()
:当数据发生变化时,调用trigger()
更新所有订阅者。notify()
:通知订阅者执行更新操作,例如重新渲染。
4.addSub
函数
function addSub(link: Link) {
link.dep.sc++ // 增加订阅者计数
if (link.sub.flags & EffectFlags.TRACKING) {
const computed = link.dep.computed
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
// 如果是计算属性,递归添加其依赖
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
}
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
link.dep.subs = link // 将订阅者添加到依赖的订阅链表
}
}
- 将
Link
添加到Dep
的订阅者链表中。 - 计算属性(
computed
)在首次订阅时需要递归订阅其所有依赖。 - 维护双向链表,确保订阅者可以在依赖发生变化时被通知。
5.track
和 trigger
结合:发布订阅模式
export function track(target: object, type: TrackOpTypes, key: unknown): void {
if (shouldTrack && activeSub) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map())) // 初始化 target 的 depsMap
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep())) // 初始化 dep
dep.map = depsMap
dep.key = key
}
dep.track() // 记录依赖
}
}
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 || key >= newLength) {
run(dep)
}
})
} else {
// 根据不同类型的操作通知相关依赖
if (key !== void 0 || depsMap.has(void 0)) {
run(depsMap.get(key))
}
if (isArrayIndex) {
run(depsMap.get(ARRAY_ITERATE_KEY))
}
}
}
endBatch() // 批量更新结束
}
track()
:用于记录数据的访问,建立target
(目标对象)到Dep
(依赖)之间的关系,确保数据变化时能够触发更新。trigger()
:当数据变化时,触发所有相关的订阅者(Effect
)更新。
除了发布订阅核心方法外,前面的MutableReactiveHandler类还用到了toRaw以及Reflect,我们也看来看看其作用
6.toRaw函数 返回响应式对象原始值
export function toRaw<T>(observed: T): T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
toRaw<T>(observed: T): T
:
- 这是一个泛型函数,接受一个
observed
参数,它是一个响应式对象
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
:
- 首先检查
observed
是否存在 - 然后通过类型断言
(observed as Target)
将observed
强制转换为Target
类型,这个类型是 Vue 3 响应式系统中定义的原始数据类型。 [ReactiveFlags.RAW]
是 Vue 3 中定义的一个常量,它用来标记响应式对象的原始数据。每个响应式对象都在其内部存储了一个标记为RAW
的属性,指向该对象的原始数据。
return raw ? toRaw(raw) : observed
:
- 如果
raw
存在,说明observed
是一个响应式对象,raw
是它对应的原始对象。此时会递归调用toRaw(raw)
,继续向上解开代理,直到找到原始对象为止。 - 如果
raw
不存在,说明observed
本身就是原始对象,直接返回它。
7.为什么 Proxy
中使用 Reflect
首先需要先了解Reflect
如何使用,传送门:Reflect
Reflect
和 Proxy
的关系
Proxy
用于拦截对象操作,允许你定义自定义的行为来替代默认的操作(如get
、set
等)。Reflect
提供了与Proxy
方法对应的操作方法,允许你在Proxy
中执行默认的目标对象操作,并确保这些操作的一致性和可靠性。
Proxy
经常用来拦截对象的操作(例如 get
、set
)。为了确保 Proxy
的拦截行为与原生 JavaScript 对象的行为一致,Vue 会在 Proxy
的 handler
方法中调用 Reflect
方法来执行实际的对象操作。
例如:
const handler = {
get(target, prop, receiver) {
// 通过 Reflect.get 调用目标对象的 get 方法
return Reflect.get(...arguments);
},
set(target, prop, value, receiver) {
// 通过 Reflect.set 调用目标对象的 set 方法
return Reflect.set(...arguments);
}
};
总结
Proxy
是用来拦截和定制对象操作的,而Reflect
是用来执行目标对象的原生操作的。Proxy
和Reflect
提供了相互补充的功能,Proxy
用来拦截和定制行为,而Reflect
用来执行实际的操作。Proxy
用于实现响应式对象的拦截,而Reflect
用来确保目标对象的操作一致性和可靠性。
断点调试(超详细图解)
创建reactive
对象
1.从创建reactive
对象开始
2.进入reactive
方法里,可以看到我们的target已经赋值了原始对象
3.判断完对象为可读后,进入到createReactiveObject
方法,我们看看此时的参数赋值了什么
4.判断target
为正常对象且此时还不是Proxy
对象时,进入到getTargetType
方法,进行类型判断
5.经过了对象判断,进入到targetTypeMap
方法,判断为普通对象类型,输出TargetType.COMMON
6.判断对象是否已经存在对应的Proxy
了,有的话则返回
7.经过重重校验后,终于创建成功了新的Proxy
实例,并将新的Proxy
缓存到proxyMap
改变reactive
对象
1.从修改reactive
对象开始
2.进入到MutableReactiveHandler
类中,先看参数赋值,这一步获取当前属性的旧值
MutableReactiveHandler
类上面文章详讲过,不了解的前往:手摸手带你阅读Vue3源码之Reactive 上
3.进入非浅响应判断,先获取旧值是否只读
4.进入新值和旧值都不是浅层响应和只读的判断,并且把旧值跟新值的原始值存起来
5.下个判断为如果旧值是 ref 类型而新值不是 ref,不符合条件,跳过到下一步
6.判断目标对象是否已经存在该属性
7.使用 Reflect.set 执行属性赋值
8.进入下一个判断如果目标对象没有被代理(即没有包装成 Proxy)
9.已有属性值,并且属性值发生变化,触发 SET 操作
10.进入到trigger
函数中,先看传参值,这一步从从全局 targetMap
获取当前 target
对象的 depsMap
11.target
没有被追踪过,直接返回,增加全局版本号
12.return出trigger
函数,回到MutableReactiveHandler
类的set
方法,返回true
修改完毕
总结
reactive
函数实际上调用了createReactiveObject
方法。createReactiveObject
负责创建一个proxy
实例,并为代理对象添加getter
和setter
行为,这些行为是在mutableHandlers
对象中定义的。- 在改变属性时,会触发
MutableReactiveHandler
中的set
方法 - 当新值被设置时,
set
方法会触发trigger
函数,进而触发依赖的更新 - 在
trigger
中,从targetMap
中根据目标对象和属性名(key
)获取对应的副作用函数,然后执行该函数,从而完成依赖的触发。