各位观众老爷们,大家好!今天咱们就来聊聊 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 环节
好了,今天的讲座就到这里。大家有什么问题吗?欢迎提问!