各位观众老爷们,大家好!今天咱们就来聊聊 Vue 3 源码里一个非常关键的角色——ReactiveEffect
,以及它如何巧妙地利用 WeakMap
和 Set
来管理响应式系统的依赖关系,构建一个高效的依赖关系图。准备好了吗?Let’s dive in!
开场:响应式系统的基石
响应式系统是现代前端框架的核心。Vue 3 也不例外。当数据发生变化时,能够自动更新视图,这背后的功臣就是响应式系统。ReactiveEffect
就像是这个系统里的侦察兵,时刻监听着数据的变化,并通知相关的视图进行更新。
ReactiveEffect
是何方神圣?
ReactiveEffect
类本质上是一个包装函数,它包含以下几个关键要素:
fn
: 这是要执行的函数。通常,这个函数会读取一些响应式数据。scheduler
(可选): 一个调度器函数,用于控制 effect 何时执行。如果没有提供,则默认同步执行。deps
: 一个Set
数组,存储着当前 effect 依赖的所有Tracked
的target/key
组合。
简单来说,ReactiveEffect
记录了某个函数(fn
)依赖了哪些响应式数据。一旦这些数据发生变化,ReactiveEffect
就会重新执行 fn
。
WeakMap
和 Set
的巧妙配合
Vue 3 使用 WeakMap
和 Set
来构建一个高效的依赖关系图。这个图的结构大致如下:
WeakMap<Target, Map<Key, Set<ReactiveEffect>>>
咱们来一层一层地解读这个结构:
Target
: 指的是被侦听的响应式对象(例如,一个 JavaScript 对象)。Key
: 指的是响应式对象上的属性名。Set<ReactiveEffect>
: 一个Set
集合,存储着所有依赖于该Target
对象的Key
属性的ReactiveEffect
实例。
为什么使用 WeakMap
和 Set
?
WeakMap
: 使用WeakMap
的关键在于它的“弱引用”特性。这意味着,如果一个Target
对象不再被其他地方引用,那么WeakMap
中对应的条目会被垃圾回收器自动清除,从而避免内存泄漏。Set
: 使用Set
的好处是可以保证ReactiveEffect
实例的唯一性。一个ReactiveEffect
不会被重复添加到依赖集合中。
核心代码剖析:track
函数
track
函数是建立依赖关系的关键。当一个 ReactiveEffect
实例在执行 fn
函数时,如果读取了响应式数据,track
函数就会被调用,将该 ReactiveEffect
实例添加到依赖关系图中。
// packages/reactivity/src/effect.ts
import { isTracking } from './reactive';
import { Dep } from './dep';
const targetMap = new WeakMap<object, Map<unknown, Set<ReactiveEffect>>>();
export let activeEffect: ReactiveEffect | undefined;
export class ReactiveEffect<T = any> {
active = true;
deps: Dep[] = []; // 存储effect依赖的dep
parent: ReactiveEffect | undefined = undefined;
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope);
}
run() {
if (!this.active) {
return this.fn();
}
let parent: ReactiveEffect | undefined = activeEffect;
let lastShouldTrack = shouldTrack;
try {
activeEffect = this;
shouldTrack = true;
return this.fn();
} finally {
activeEffect = parent;
shouldTrack = lastShouldTrack;
}
}
stop() {
if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
export type EffectScheduler = (...args: any[]) => any;
export function effect<T>(fn: () => T, options: ReactiveEffectOptions = {}): ReactiveEffectRunner {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
runner.effect = _effect;
return runner;
}
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
trackEffects(dep);
}
export function trackEffects(dep: Dep) {
if (activeEffect) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
export function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
export let shouldTrack = true;
export function pauseTracking() {
shouldTrack = false;
}
export function enableTracking() {
shouldTrack = true;
}
export function resetTracking() {
shouldTrack = true;
activeEffect = undefined;
}
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
dep.delete(effect);
}
deps.length = 0;
}
}
export interface ReactiveEffectOptions {
scheduler?: EffectScheduler;
scope?: EffectScope;
lazy?: boolean;
allowRecurse?: boolean;
onStop?: () => void;
}
export interface ReactiveEffectRunner<T = any> {
(): T;
effect: ReactiveEffect;
}
export const enum TrackOpTypes {
GET,
HAS,
ITERATE
}
export const enum TriggerOpTypes {
SET,
ADD,
DELETE,
CLEAR
}
让我们逐行分析 track
函数:
if (!isTracking()) { return; }
: 首先检查是否处于追踪状态。只有在activeEffect
存在且shouldTrack
为true
时,才进行依赖追踪。let depsMap = targetMap.get(target);
: 尝试从targetMap
中获取target
对象对应的depsMap
。if (!depsMap) { ... }
: 如果depsMap
不存在,则创建一个新的Map
,并将其存储到targetMap
中。let dep = depsMap.get(key);
: 尝试从depsMap
中获取key
属性对应的dep
(Set)。if (!dep) { ... }
: 如果dep
不存在,则创建一个新的Set
,并将其存储到depsMap
中。dep.add(activeEffect);
: 将当前激活的activeEffect
添加到dep
集合中。
trigger
函数:触发更新
当响应式数据发生变化时,trigger
函数会被调用,用于通知所有依赖于该数据的 ReactiveEffect
实例进行更新。
// packages/reactivity/src/effect.ts (继续)
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: any,
oldValue?: any
) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let deps: (Dep | undefined)[] = [];
if (key !== void 0) {
deps.push(depsMap.get(key));
}
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
if (key === 'length' && isArray(target)) {
deps.push(depsMap.get('length'));
} else {
deps.push(depsMap.get(ITERATE_KEY));
}
}
if (type === TriggerOpTypes.ADD && isArray(target) && isIntegerKey(key)) {
deps.push(depsMap.get('length'));
}
const effects: ReactiveEffect[] = [];
for (const dep of deps) {
if (dep) {
effects.push(...dep);
}
}
triggerEffects(createDep(effects));
}
export function triggerEffects(dep: Dep) {
const effects = isArray(dep) ? dep : Array.from(dep);
for (const effect of effects) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
import { isArray } from '@vue/shared';
import { isIntegerKey } from '@vue/shared';
import { ITERATE_KEY } from './reactive';
import { createDep } from './dep';
让我们逐行分析 trigger
函数:
const depsMap = targetMap.get(target);
: 从targetMap
中获取target
对象对应的depsMap
。if (!depsMap) { return; }
: 如果depsMap
不存在,说明没有 effect 依赖于该target
对象,直接返回。let deps: (Dep | undefined)[] = [];
初始化deps
数组,用于存储需要触发的 dep 集合。if (key !== void 0) { deps.push(depsMap.get(key)); }
: 如果key
存在,则将depsMap
中key
对应的dep
添加到deps
数组中。- 处理数组的特殊情况: 针对数组的
ADD
和DELETE
操作,需要触发length
属性和ITERATE_KEY
对应的依赖。 triggerEffects(createDep(effects));
: 遍历effects
数组,依次执行每个ReactiveEffect
实例。如果ReactiveEffect
实例有scheduler
,则调用scheduler
,否则调用run
函数。
一个简单的例子
import { reactive, effect } from '@vue/reactivity';
const obj = reactive({
count: 0
});
effect(() => {
console.log("Count is:", obj.count);
});
obj.count++; // 输出: Count is: 1
在这个例子中,我们创建了一个响应式对象 obj
,并使用 effect
函数创建了一个 ReactiveEffect
实例。当 obj.count
的值发生变化时,ReactiveEffect
实例会自动重新执行,打印出新的 count
值。
依赖收集的过程:
reactive(obj)
: 将obj
转化为响应式对象,内部使用Proxy
拦截get
和set
操作。effect(() => { console.log("Count is:", obj.count); })
: 创建一个ReactiveEffect
实例,并立即执行它的run
方法。- 在
run
方法中,activeEffect
被设置为当前的ReactiveEffect
实例。 - 当执行
obj.count
时,会触发Proxy
的get
拦截器。 - 在
get
拦截器中,会调用track(obj, TrackOpTypes.GET, 'count')
函数,将当前的ReactiveEffect
实例添加到targetMap
中,建立依赖关系。
触发更新的过程:
obj.count++
: 修改obj.count
的值,触发Proxy
的set
拦截器。- 在
set
拦截器中,会调用trigger(obj, TriggerOpTypes.SET, 'count')
函数,通知所有依赖于obj.count
的ReactiveEffect
实例进行更新。 trigger
函数会从targetMap
中找到obj.count
对应的ReactiveEffect
实例,并执行它们的run
方法,从而更新视图。
源码中的关键点
activeEffect
: 这是一个全局变量,用于存储当前正在执行的ReactiveEffect
实例。在track
函数中,我们可以通过activeEffect
访问到当前激活的ReactiveEffect
实例。shouldTrack
: 这是一个全局变量,用于控制是否进行依赖追踪。在某些情况下,我们可能需要临时禁用依赖追踪,例如在执行一些不需要触发更新的操作时。cleanupEffect
: 在ReactiveEffect
停止时,需要调用cleanupEffect
函数来清除所有依赖。这个函数会遍历ReactiveEffect
实例的deps
数组,从每个dep
集合中删除该ReactiveEffect
实例。
Dep
类:解耦Effect和依赖
从上面代码可以发现,Vue3新加了Dep
类,用来维护和管理ReactiveEffect
实例。
// packages/reactivity/src/dep.ts
export type Dep = Set<ReactiveEffect> & TrackedDeps
type TrackedDeps = {
w: number
n: number
}
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0
dep.n = 0
return dep
}
为什么要使用 Dep 类?
- 解耦: 将 Effect 和依赖关系解耦,使得依赖关系的管理更加灵活。Effect 只需关注自身逻辑,无需关心依赖关系的具体实现。
- 优化: Dep 类可以进行一些优化操作,例如去重、排序等,从而提高依赖更新的效率。
- 扩展性: Dep 类可以方便地进行扩展,例如添加新的依赖类型、支持更多高级功能。
总结
Vue 3 使用 WeakMap
和 Set
构建了一个高效的依赖关系图,使得响应式系统能够快速准确地追踪数据的变化,并更新相关的视图。ReactiveEffect
就像是这个系统里的侦察兵,时刻监听着数据的变化,并通知相关的视图进行更新。通过理解 ReactiveEffect
类以及 track
和 trigger
函数的实现原理,我们可以更好地理解 Vue 3 响应式系统的核心机制。
Q&A 环节
好了,今天的讲座就到这里。大家有什么问题吗?欢迎提问!