剖析 Vue 3 源码中 `ReactiveEffect` 类如何利用 `WeakMap` 和 `Set` 数据结构高效地管理依赖关系图。

各位观众老爷们,大家好!今天咱们就来聊聊 Vue 3 源码里一个非常关键的角色——ReactiveEffect,以及它如何巧妙地利用 WeakMapSet 来管理响应式系统的依赖关系,构建一个高效的依赖关系图。准备好了吗?Let’s dive in!

开场:响应式系统的基石

响应式系统是现代前端框架的核心。Vue 3 也不例外。当数据发生变化时,能够自动更新视图,这背后的功臣就是响应式系统。ReactiveEffect 就像是这个系统里的侦察兵,时刻监听着数据的变化,并通知相关的视图进行更新。

ReactiveEffect 是何方神圣?

ReactiveEffect 类本质上是一个包装函数,它包含以下几个关键要素:

  1. fn: 这是要执行的函数。通常,这个函数会读取一些响应式数据。
  2. scheduler (可选): 一个调度器函数,用于控制 effect 何时执行。如果没有提供,则默认同步执行。
  3. deps: 一个 Set 数组,存储着当前 effect 依赖的所有 Trackedtarget/key 组合。

简单来说,ReactiveEffect 记录了某个函数(fn)依赖了哪些响应式数据。一旦这些数据发生变化,ReactiveEffect 就会重新执行 fn

WeakMapSet 的巧妙配合

Vue 3 使用 WeakMapSet 来构建一个高效的依赖关系图。这个图的结构大致如下:

WeakMap<Target, Map<Key, Set<ReactiveEffect>>>

咱们来一层一层地解读这个结构:

  • Target: 指的是被侦听的响应式对象(例如,一个 JavaScript 对象)。
  • Key: 指的是响应式对象上的属性名。
  • Set<ReactiveEffect>: 一个 Set 集合,存储着所有依赖于该 Target 对象的 Key 属性的 ReactiveEffect 实例。

为什么使用 WeakMapSet

  • 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 函数:

  1. if (!isTracking()) { return; }: 首先检查是否处于追踪状态。只有在 activeEffect 存在且 shouldTracktrue 时,才进行依赖追踪。
  2. let depsMap = targetMap.get(target);: 尝试从 targetMap 中获取 target 对象对应的 depsMap
  3. if (!depsMap) { ... }: 如果 depsMap 不存在,则创建一个新的 Map,并将其存储到 targetMap 中。
  4. let dep = depsMap.get(key);: 尝试从 depsMap 中获取 key 属性对应的 dep (Set)。
  5. if (!dep) { ... }: 如果 dep 不存在,则创建一个新的 Set,并将其存储到 depsMap 中。
  6. 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 函数:

  1. const depsMap = targetMap.get(target);: 从 targetMap 中获取 target 对象对应的 depsMap
  2. if (!depsMap) { return; }: 如果 depsMap 不存在,说明没有 effect 依赖于该 target 对象,直接返回。
  3. let deps: (Dep | undefined)[] = []; 初始化 deps 数组,用于存储需要触发的 dep 集合。
  4. if (key !== void 0) { deps.push(depsMap.get(key)); }: 如果 key 存在,则将 depsMapkey 对应的 dep 添加到 deps 数组中。
  5. 处理数组的特殊情况: 针对数组的ADDDELETE操作,需要触发length属性和ITERATE_KEY对应的依赖。
  6. 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 值。

依赖收集的过程:

  1. reactive(obj): 将 obj 转化为响应式对象,内部使用 Proxy 拦截 getset 操作。
  2. effect(() => { console.log("Count is:", obj.count); }): 创建一个 ReactiveEffect 实例,并立即执行它的 run 方法。
  3. run 方法中,activeEffect 被设置为当前的 ReactiveEffect 实例。
  4. 当执行 obj.count 时,会触发 Proxyget 拦截器。
  5. get 拦截器中,会调用 track(obj, TrackOpTypes.GET, 'count') 函数,将当前的 ReactiveEffect 实例添加到 targetMap 中,建立依赖关系。

触发更新的过程:

  1. obj.count++: 修改 obj.count 的值,触发 Proxyset 拦截器。
  2. set 拦截器中,会调用 trigger(obj, TriggerOpTypes.SET, 'count') 函数,通知所有依赖于 obj.countReactiveEffect 实例进行更新。
  3. 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 类?

  1. 解耦: 将 Effect 和依赖关系解耦,使得依赖关系的管理更加灵活。Effect 只需关注自身逻辑,无需关心依赖关系的具体实现。
  2. 优化: Dep 类可以进行一些优化操作,例如去重、排序等,从而提高依赖更新的效率。
  3. 扩展性: Dep 类可以方便地进行扩展,例如添加新的依赖类型、支持更多高级功能。

总结

Vue 3 使用 WeakMapSet 构建了一个高效的依赖关系图,使得响应式系统能够快速准确地追踪数据的变化,并更新相关的视图。ReactiveEffect 就像是这个系统里的侦察兵,时刻监听着数据的变化,并通知相关的视图进行更新。通过理解 ReactiveEffect 类以及 tracktrigger 函数的实现原理,我们可以更好地理解 Vue 3 响应式系统的核心机制。

Q&A 环节

好了,今天的讲座就到这里。大家有什么问题吗?欢迎提问!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注