版本:Vue 3.5.17
1. 核心概念
effect 是 Vue 3 响应式系统的核心部分,主要负责依赖追踪和自动响应。它通过 ReactiveEffect 类来封装副作用逻辑,实现依赖收集和触发更新的功能。并且,computed、watch、组件渲染函数等高级 API 在底层都依赖于 effect 来实现。
2. 基本定义
effect 函数用于创建一个响应式副作用。
ts
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = {}
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const _effect = new ReactiveEffect(fn, options.scheduler)
if (!options.lazy) {
_effect.run()
}
return _effect
}
- 首先会检查传入的
fn
是否本身就是一个ReactiveEffect
实例,如果是,则取其raw
属性(指向原始的用户传入函数)。 - 然后创建一个
ReactiveEffect
实例,传入用户的副作用函数fn
以及调度器函数scheduler
(如果有提供的话)。 - 如果没有设置
lazy
选项(默认为false
),则立即调用_effect.run()
来执行副作用函数并进行初始的依赖收集。 - 最后返回创建的
ReactiveEffect
实例。
3. ReactiveEffect
类
简化代码:
ts
export class ReactiveEffect<T = any> {
active = true;
deps: Dep[] = [];
parent: ReactiveEffect | undefined;
constructor(
public fn: () => T,
public scheduler: (() => void) | undefined
) {}
run() {
if (!this.active) {
return this.fn()
}
try {
this.parent = activeEffect;
activeEffect = this;
cleanupEffect(this);
return this.fn()
} finally {
activeEffect = this.parent;
this.parent = undefined;
}
}
stop() {
if (this.active) {
cleanupEffect(this);
this.active = false;
}
}
}
-
属性:
active
:表示该effect
是否处于活动状态,默认值为true
。deps
:一个数组,存储了该effect
所依赖的所有依赖集合(Dep
类型,本质是Set<ReactiveEffect>
)。parent
:用于处理嵌套effect
的情况,指向父级ReactiveEffect
实例。
-
run
方法:- 首先检查
active
状态,如果为false
,直接执行用户传入的fn
函数并返回结果。 - 然后将当前
activeEffect
赋值给parent
,并把自身设置为新的activeEffect
,这是依赖收集的关键步骤,让系统知道当前正在执行的是哪个effect
。 - 调用
cleanupEffect
方法清理之前收集的旧依赖,避免无效依赖残留。 - 执行用户传入的副作用函数
fn
,在执行过程中,如果访问了响应式对象的属性,就会触发依赖收集。 - 最后在
finally
块中,恢复activeEffect
为之前保存的父级effect
,并清空parent
。
- 首先检查
stop
方法 :用于停止该effect
的响应式行为,先调用cleanupEffect
清理依赖,然后将active
设置为false
。
4. 依赖收集(track 函数)
当访问响应式对象的属性时,会触发 Proxy
的 get
捕获器,进而调用 track
函数进行依赖收集:
ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!activeEffect || shouldSkipTrack()) {
return
}
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 Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
- 首先检查当前是否存在活跃的
activeEffect
以及是否需要跳过依赖收集(比如在一些特定场景下,如readonly
对象的访问)。 - 然后通过
targetMap
(一个全局的WeakMap<object, Map<key, Set<ReactiveEffect>>>
)获取与目标对象target
对应的depsMap
。如果不存在,则创建一个新的Map
并设置到targetMap
中。 - 接着从
depsMap
中获取与属性key
对应的依赖集合dep
,如果不存在,同样创建一个新的Set
并设置到depsMap
中。 - 最后检查当前
activeEffect
是否已经存在于dep
中,如果不存在,则将其添加到dep
中,并且将dep
添加到activeEffect.deps
中,建立双向的依赖关系,方便后续清理。
5. 触发更新(trigger 函数)
当修改响应式对象的属性时,会触发 Proxy
的 set
捕获器,进而调用 trigger
函数来通知相关的 effect
重新执行:
ts
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect!== activeEffect || type === TriggerOpTypes.CLEAR) {
effects.add(effect)
}
})
}
}
if (type === TriggerOpTypes.CLEAR) {
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
if (key!== void 0) {
add(depsMap.get(key))
}
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
add(depsMap.get('*'))
} else if (isIntegerKey(key)) {
add(depsMap.get('length'))
}
break
}
}
const run = (effect: ReactiveEffect) => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
effects.forEach(run)
}
- 首先从
targetMap
中获取与目标对象target
对应的depsMap
,如果不存在则直接返回。 - 定义一个
add
函数,用于将需要重新执行的effect
添加到effects
集合中,并且会根据一些条件(如当前activeEffect
是否是正在触发更新的effect
等)进行过滤。 - 根据不同的操作类型(
type
,如CLEAR
、ADD
、属性设置等),从depsMap
中获取相关的依赖集合,并调用add
函数添加到effects
中。 - 最后遍历
effects
集合,对于每个effect
,如果其定义了scheduler
调度器函数,则调用调度器(如watch
中会利用调度器实现异步回调等逻辑),否则直接调用effect.run()
重新执行副作用函数。
6. 调度器(scheduler)
effect
可以接受一个可选的 scheduler
函数作为选项,用于自定义 effect
重新执行的方式。例如,在 watch
中,通过设置 scheduler
可以将回调函数加入微任务队列,实现异步批处理更新,避免不必要的同步更新操作,提升性能。
ts
// 示例:watch 中使用 scheduler
watch(source, (newValue, oldValue) => {
// 业务逻辑
}, {
scheduler: () => {
queueJob(() => {
// 实际执行的回调逻辑
})
}
})
7. 嵌套 effect
的处理
Vue 3 支持嵌套的 effect
,通过维护一个 effectStack
栈结构来跟踪当前活跃的 effect
。当进入一个新的 effect
时,将其压入栈中,并将 activeEffect
设置为当前 effect
;当执行完毕后,将其从栈中弹出,并恢复 activeEffect
为之前的值。这样可以确保在嵌套场景下,每个属性访问都能正确关联到当前正在执行的 effect
,避免依赖收集混乱。
8. 清理机制(cleanupEffect
函数)
在每次 effect
执行前,会调用 cleanupEffect
函数来清理之前收集的旧依赖:
ts
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0, len = deps.length; i < len; i++) {
deps[i].delete(effect)
}
effect.deps.length = 0
}
}
该函数遍历 effect.deps
中的所有依赖集合,将当前 effect
从这些集合中删除,然后清空 effect.deps
数组。这样做可以避免在动态分支逻辑(如 v-if
切换导致的条件依赖变化)中,残留无效的依赖关系,防止内存泄漏和多次重复调用。
9. 总结
总之,Vue 3.5.17 中 effect
相关的机制通过 ReactiveEffect
类、依赖收集、触发更新、调度器等一系列设计,构成了一个功能强大且灵活的响应式系统,为 Vue 应用的数据响应式更新提供了坚实的基础 。
