reactiveEffect.ts:Vue 3响应式系统的核心
- 1. 什么是 reactiveEffect?
- 2. 核心机制
- 2.1 依赖收集(Track)
- 2.2 触发更新(Trigger)
- 2.3 效果范围(effectScope)
- 3. 源码解析 —— track
- 3.1 track
- 3.2 参数 target 究竟是什么
- 3.2.1 target是什么
- 3.2.2 怎么理解target
- 3.2.3 小结
- 4. 源码解析 —— trigger
- 5. 应用场景
- 5.1 组件渲染
- 5.2 计算属性
- 5.3 观察者(watch)
- 6. 小结
Vue 3的响应式系统是基于Proxy和Reflect API构建的,其中reactiveEffect扮演了核心角色,实现了数据的响应式变化追踪和视图的自动更新。本章节我们将深入探讨reactiveEffect的工作原理。
1. 什么是 reactiveEffect?
在Vue中,每当我们操作响应式数据时,都会触发reactiveEffect来跟踪这些操作。无论是计算属性、watch监听器,还是组件的渲染函数,都被视为副作用函数,并被reactiveEffect所管理。
2. 核心机制
2.1 依赖收集(Track)
track函数负责在副作用函数首次执行时收集所有被访问的响应式数据的依赖。这意味着,当一个数据项被读取时,所有依赖于这个数据的副作用函数都会被记录下来。
2.2 触发更新(Trigger)
当响应式数据变化时,trigger函数会找到所有依赖于这个数据的副作用函数,并重新执行它们,从而实现数据到视图的自动更新。
2.3 效果范围(effectScope)
effectScope是Vue 3引入的新概念,它允许开发者组织和管理多个reactiveEffect。这对于在组件卸载或需要清理副作用时非常有用,避免内存泄漏。
3. 源码解析 —— track
3.1 track
/**
* track函数负责追踪一个响应式对象的属性访问操作
*
* 它的主要作用是确定当前哪个副作用函数(effect)正在运行
* 并将这个副作用函数记录为该响应式属性的依赖
*
* @param target - 被访问属性所属的响应式对象.
* @param type - 访问类型,由TrackOpTypes枚举定义,比如读取、写入.
* @param key - 被访问的响应式属性的标识符.
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
/**
* 1.条件判断: 首先检查是否应该进行依赖收集,这由shouldTrack和activeEffect共同决定。
* shouldTrack是一个标志,表明是否应该收集依赖;
* activeEffect指的是当前正在执行的副作用函数。
*/
if (shouldTrack && activeEffect) {
/**
* 2.获取依赖映射: 使用targetMap(一个全局WeakMap,在上一章节详细介绍过)尝试获取当前对象的依赖映射depsMap。
* 如果不存在,就为这个对象创建一个新的Map并设置到targetMap中。
*/
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
/**
* 3. 获取依赖集合: 尝试从depsMap中获取对应属性key的依赖集合dep。
* 如果这个属性还没有依赖集合,就创建一个新的依赖集合,并设置一个清理函数,当依赖集合为空时从depsMap中移除这个属性的记录
*/
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
}
/**
* 4. 追踪效果: 最后,使用trackEffect函数将当前的activeEffect(副作用函数)添加到这个属性的依赖集合中。
* 如果是开发模式(__DEV__为true),还会传递额外的调试信息,包括被访问的对象、访问类型和属性标识符。
*/
trackEffect(
activeEffect,
dep,
__DEV__
? {
target,
type,
key,
}
: void 0,
)
}
}
3.2 参数 target 究竟是什么
在 baseHandlers.ts 里的get函数里,我们说target是原始对象,而在track函数里,我们说target是被访问属性所属的响应式对象,但是它们又是同一个值,这让人很困惑。
3.2.1 target是什么
看一下proxy里关于捕获器的实例:
const target ={
foo: 'bar'
}
const handler={
get(trapTarget, property, receiver) {
console.log(trapTarget === target);
console.log(property);
console.log(receiver === proxy);
}
}
const proxy = new Proxy(target, handler);
proxy.foo;
//输出
//true
//foo
//true
所以在技术上无论是get函数里,还是track函数里,target是原始对象。
3.2.2 怎么理解target
看一下proxy最基础的示例:
const target={
id:'target'
};
const handle={};
const proxy = new Proxy(target,handle);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标(target)属性赋值会反映在两个对象上 ,因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo
3.2.3 小结
简而言之,尽管track的参数是原始对象,但在Vue的响应式系统上下文中,我们可以将其等同于它的响应式代理,因为所有操作实际上都是针对这个代理执行的。
4. 源码解析 —— trigger
/**
* trigger函数负责找到所有依赖于这些数据的副作用函数,
* 并执行它们以更新视图或执行其他副作用
*
* @param target - 发生变化的响应式对象.
* @param type - 变化的类型,由TriggerOpTypes枚举定义,如设置(SET)、添加(ADD)、删除(DELETE)等.
* @param key - 发生变化的具体属性名.
*/
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown, //新值(对于SET操作)
oldValue?: unknown, //旧值(对于SET操作)
oldTarget?: Map<unknown, unknown> | Set<unknown>, //旧的集合对象(对于集合类型,如Map、Set的变化)
) {
/**
* 寻找依赖:如果depsMap不存在,意味着target从未被追踪过依赖,直接返回
*/
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
/**
* 确定需要触发的依赖集合(deps):根据变化的类型(type)和具体的属性(key),确定需要触发的依赖集合
* 特殊情况处理:如整个集合被清除(CLEAR)、数组长度变化、Map集合的特定操作等,都有针对性的逻辑来确定哪些依赖需要被触发
*/
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on 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)) {
// new index added to array -> length changes
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
}
}
/**
* 触发依赖更新:
* 遍历所有需要触发的依赖集合deps,对于每个依赖(即副作用函数集合),调用triggerEffects函数来实际执行它们。
* 在执行依赖前,调用pauseScheduling暂停调度(防止在依赖执行过程中产生无限循环调用),执行完后调用resetScheduling恢复调度。
*/
pauseScheduling()
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty,
__DEV__
? {
target,
type,
key,
newValue,
oldValue,
oldTarget,
}
: void 0,
)
}
}
resetScheduling()
}
5. 应用场景
5.1 组件渲染
Vue 组件的渲染过程本身是一个副作用。Vue 使用reactiveEffect来自动重新渲染组件,当组件依赖的响应式数据变化时。
5.2 计算属性
计算属性是基于其它响应式数据计算得出的值。Vue内部使用reactiveEffect来跟踪计算属性依赖的数据,确保它们在依赖数据变化时更新。
5.3 观察者(watch)
Vue的watchAPI允许你观察响应式数据的变化,并在变化时执行回调函数。这背后也是使用reactiveEffect实现的。
6. 小结
通过本章的学习,我们了解了reactiveEffect在Vue 3响应式系统中的核心作用:依赖收集与更新触发。
而这里涉及的一些关键逻辑,如:shouldTrack、activeEffect等,我们会在下一个章节继续探讨。