前景回顾
各位看官,上回书说到,Object.defineProperty
和Proxy
在Vue的生命中都是非常重要的一员,即使到了Vue3也是难舍难分,各有好处,也不知道尤大是怎么找到的。
不如我们深入源码,再看看这块田里,咱们能挖到什么。
依赖收集和依赖分发
参考文件:packages/reactivity/src/reactiveEffect.ts
发布 - 订阅模式
发布 - 订阅模式是一种设计模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当主题对象的状态发生变化时,它会通知所有观察者对象,使它们能够自动更新自己。
Vue3的响应式原理就是基于发布 - 订阅模式实现的,它通过Proxy对象来代理原始数据对象,拦截其读取和修改的操作,从而实现对数据的依赖收集和依赖分发。
依赖收集
依赖收集的目的是为了建立数据和副作用函数之间的映射关系,让数据知道哪些副作用函数依赖于它,当数据变化时,能够通知这些副作用函数进行更新。
什么是副作用函数(effect)
副作用函数是Vue3中一个非常重要的概念,它是指那些依赖于响应式数据变化而执行的函数,比如:
- 渲染函数:负责将数据渲染到视图层
- 计算属性:根据数据计算出一个新的值
- 侦听器:监听数据的变化并执行一些操作
- 自定义函数:用户自定义的需要响应数据变化的函数
track函数
track
函数是Vue3响应式系统的核心部分之一,它的主要作用是跟踪对响应式对象属性的访问。
当你访问一个响应式对象的属性时,track
函数会被调用。它会检查当前是否有一个活动的effect(副作用函数)。如果有,那么这个effect就会被添加到这个属性的依赖列表中。这样,当这个属性的值发生变化时,所有依赖于这个属性的effect就会被重新执行(trigger
函数)。
这是Vue3实现数据响应式的关键机制。当你在组件中使用ref
或reactive
创建响应式数据,然后在Template或Computed中使用这些数据时,Vue就会自动跟踪这些依赖关系。当数据发生变化时,Vue知道需要重新渲染哪些组件 或重新计算哪些计算属性。
track
函数的实现如下:
typescript
/**
* 跟踪对响应式属性的访问。
*
* 这将检查当前正在运行的效果,并将其记录为dep,
* dep记录了所有依赖于响应式属性的效果。
*
* @param target - 持有响应式属性的对象。
* @param type - 定义对响应式属性的访问类型。
* @param key - 要跟踪的响应式属性的标识符。
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
// shouldTrack和activeEffect都为真时,才进行跟踪
if (shouldTrack && activeEffect) {
// 从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 = createDep(() => depsMap!.delete(key))))
}
// 跟踪效果
// activeEffect - 当前活动的效果
// dep - 属性的依赖
// __DEV__ ? {target, type, key} : void 0 - 在开发模式下,传递额外的调试信息
trackEffect(
activeEffect,
dep,
__DEV__
? {
target,
type,
key,
}
: void 0,
)
}
}
依赖分发
依赖分发的目的是为了在数据发生变化时,触发依赖于该数据的副作用函数进行更新,从而实现数据和视图的同步。
如何做的?
trigger
函数在Vue3的响应式系统中起着关键的作用,它负责在数据发生变化时触发更新。以下是trigger
函数的详细工作流程:
- 获取依赖映射 :首先,
trigger
函数会从targetMap
中获取目标对象的依赖映射。targetMap
是一个全局的WeakMap,它存储了所有响应式对象及其对应的依赖映射。 - 确定需要触发的依赖 :然后,
trigger
函数会根据操作类型和键名来确定需要触发哪些依赖。例如,如果操作类型是CLEAR
,那么就需要触发目标对象的所有依赖。如果目标是数组且键名是length
,那么就需要触发长度变化的依赖。对于SET
、ADD
、DELETE
操作,还需要根据键名来触发相应的依赖。 - 触发依赖的效果 :最后,
trigger
函数会遍历所有需要触发的依赖,并执行其中存储的效果。这些效果通常是重新计算计算属性的值或重新渲染组件。
通过这种方式,trigger
函数实现了Vue3的数据响应式机制,使得当数据发生变化时,所有依赖于该数据的计算属性和组件都能自动更新。
trigger
函数的实现如下:
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>,
) {
// 从targetMap中获取目标对象的依赖映射
const depsMap = targetMap.get(target)
// 如果目标对象没有依赖映射,说明它从未被跟踪过,直接返回
if (!depsMap) {
return
}
let deps: (Dep | undefined)[] = []
// 如果操作类型是CLEAR,说明集合正在被清空,触发目标的所有效果
if (type === TriggerOpTypes.CLEAR) {
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// 如果目标是数组且键名是'length',则触发长度变化的效果
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
deps.push(dep)
}
})
} else {
// 对于SET、ADD、DELETE操作,触发相应的效果
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// 对于ADD、DELETE、Map.SET操作,也要触发迭代键的效果
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// 数组中添加了新索引 -> 长度变化
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
// 暂停调度
pauseScheduling()
// 触发所有依赖的效果
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty,
__DEV__
? {
target,
type,
key,
newValue,
oldValue,
oldTarget,
}
: void 0,
)
}
}
// 重置调度
resetScheduling()
}
这样,我们就完成了trigger
函数的解析,它的作用是在数据发生变化时,触发依赖于该数据的副作用函数进行更新,从而实现数据和视图的同步。
本节参考
第二节总结
本章呢,主要聊了一下源码中track
函数和trigger
函数,基本讲了一下他们是干什么的,他们在响应式中做了什么事情。
内容如有不妥,欢迎指正。
课后问题
批量异步更新 :在Vue的响应式系统中,当数据发生变化时,trigger
函数会立即触发所有依赖于该数据的effect。但是,如果在一个事件循环中,同一个数据被多次修改,那么同一个effect就会被多次触发,这可能会导致不必要的计算和渲染。Vue是如何解决这个问题的?如果你要设计一个批量异步更新的机制,你会如何设计?
