解释 Vue 3 源码中 `ReactiveEffect` 类如何管理依赖关系 (`dep` 和 `effect` 的双向绑定),以及 `stop` 函数的清理过程。

各位观众老爷,欢迎来到今天的 Vue 3 源码解剖现场!今天我们要聊的主题是 Vue 3 响应式系统的核心组件之一:ReactiveEffect。 别担心,虽然听起来很吓人,但其实它就像一个勤劳的“数据管家”,负责维护着数据变化和视图更新之间的和谐关系。

准备好了吗?让我们一起深入源码,看看这位“管家”是如何工作的。

ReactiveEffect:响应式系统的灵魂舞者

ReactiveEffect 类是 Vue 3 响应式系统的核心,它负责将依赖(dep,Dependency)和副作用(effect,执行函数)连接起来,形成一个高效的“数据-视图”同步机制。 简单来说,它做了两件事:

  1. 追踪依赖: 当一个 effect 函数执行时,它会记录下其中访问了哪些响应式数据。
  2. 触发更新: 当这些响应式数据发生变化时,它会重新执行这个 effect 函数,从而更新视图。

1. ReactiveEffect 类的基本结构

我们先来看看 ReactiveEffect 类的基本骨架:

class ReactiveEffect<T = any> {
  active = true; // effect是否激活
  deps: Dep[] = []; // 存储依赖的 Set 集合
  parent: ReactiveEffect | undefined = undefined;
  options: ReactiveEffectOptions; // 一些选项,如调度器、是否lazy等

  constructor(
    public fn: () => T, // 实际执行的函数,也就是副作用函数
    scheduler?: EffectScheduler | null,
    scope?: EffectScope | undefined
  ) {
    this.options = { scheduler };
    this.scheduler = scheduler;
    this.scope = scope;
  }

  run() {
    if (!this.active) { // 如果 effect 已经停止,直接执行 fn,不追踪依赖
      return this.fn();
    }

    let parent: ReactiveEffect | undefined = activeEffect;
    let lastShouldTrack = shouldTrack;

    try {
      this.parent = parent;
      activeEffect = this; // 将当前 effect 设为激活状态
      shouldTrack = true; // 允许追踪依赖

      cleanupEffect(this); // 清理之前的依赖

      return this.fn(); // 执行副作用函数,期间会触发 get 操作,收集依赖
    } finally {
      activeEffect = parent; // 恢复之前的激活状态
      shouldTrack = lastShouldTrack; // 恢复之前的追踪状态
    }
  }

  stop() {
    if (this.active) {
      cleanupEffect(this); // 清理所有依赖
      if (this.options.onStop) {
        this.options.onStop(); // 执行 stop 回调
      }
      this.active = false; // 标记为停止状态
    }
  }
}

这里几个关键的属性和方法需要重点关注:

  • active: 标志 effect 是否处于激活状态,只有激活状态的 effect 才会追踪依赖和执行更新。
  • deps: 存储当前 effect 依赖的所有 Dep 实例,Dep 实例负责管理依赖于某个响应式数据的所有 effect
  • fn: 实际执行的副作用函数,也就是我们希望在数据变化时重新执行的函数。
  • run(): 执行 effect 函数的核心方法,负责设置全局状态、清理旧依赖、执行副作用函数以及恢复全局状态。
  • stop(): 停止 effect 函数的执行,并清理所有相关的依赖。
  • cleanupEffect(): 用于清理副作用effect的所有依赖关系,防止内存泄漏

2. 依赖追踪:track 函数

当我们在 effect 函数中访问响应式数据时,会触发该数据的 get 操作。 在 get 操作中,Vue 会调用 track 函数来建立依赖关系。 track 函数的核心逻辑如下:

let activeEffect: ReactiveEffect | undefined;
let shouldTrack = true;

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    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 = createDep()));
  }

  trackEffects(dep);
}

export function isTracking() {
  return shouldTrack && activeEffect !== undefined;
}

export function trackEffects(dep: Dep) {
  if (!dep.has(activeEffect!)) {
    dep.add(activeEffect!);
    activeEffect!.deps.push(dep);
  }
}

其中,targetMap 是一个全局的 WeakMap,用于存储所有响应式数据的依赖关系。 其结构大致如下:

targetMap: {
  target(object): { // 响应式对象
    depsMap(Map): {
      key(string | symbol): Dep // 响应式属性
    }
  }
}

track 函数的执行流程如下:

  1. 检查是否需要追踪依赖 (isTracking() 函数)。 只有当 shouldTracktrueactiveEffect 存在时,才会进行依赖追踪。
  2. targetMap 中获取当前响应式对象对应的 depsMap。 如果不存在,则创建一个新的 depsMap
  3. depsMap 中获取当前响应式属性对应的 Dep 实例。 如果不存在,则创建一个新的 Dep 实例。
  4. 调用 trackEffects 函数,将当前激活的 effect 添加到 Dep 实例中,并将 Dep 实例添加到 effectdeps 数组中。

这样就完成了 depeffect 之间的双向绑定。

3. 触发更新:trigger 函数

当响应式数据发生变化时,会触发该数据的 set 操作。 在 set 操作中,Vue 会调用 trigger 函数来触发更新。 trigger 函数的核心逻辑如下:

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)[] = [];
  deps.push(depsMap.get(key)); // 拿到key对应的dep

  const effects: ReactiveEffect[] = [];
  for (const dep of deps) {
    if (dep) {
      effects.push(...dep);
    }
  }
  runEffects(effects);
}

export function runEffects(effects: ReactiveEffect[]) {
  const seen = new Set();
  for (const effect of effects) {
    if (effect.scheduler) {
      effect.scheduler(); // 如果有调度器,则调用调度器
    } else {
      if (!seen.has(effect)) {
        effect.run(); // 否则直接执行 effect
        seen.add(effect);
      }
    }
  }
}

trigger 函数的执行流程如下:

  1. targetMap 中获取当前响应式对象对应的 depsMap。 如果不存在,则表示该对象没有依赖,直接返回。
  2. depsMap 中获取当前响应式属性对应的 Dep 实例。
  3. 遍历 Dep 实例中的所有 effect,依次执行它们。
  4. 如果 effect 存在调度器 (scheduler),则调用调度器。 否则,直接调用 effect.run() 方法。

4. cleanupEffect 函数:依赖清理专家

cleanupEffect 函数是 ReactiveEffect 类中非常重要的一个方法,它负责清理 effect 之前收集的依赖。 它的主要作用是:

  1. 防止内存泄漏: 如果不清理旧的依赖,effect 会一直持有对旧 Dep 实例的引用,导致这些 Dep 实例无法被垃圾回收。
  2. 避免不必要的更新: 如果不清理旧的依赖,当旧的依赖数据发生变化时,effect 可能会被错误地触发,导致不必要的更新。

cleanupEffect 函数的核心逻辑如下:

function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect;
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect); // 从 dep 中移除 effect
    }
    deps.length = 0; // 清空 effect 的 deps 数组
  }
}

cleanupEffect 函数的执行流程如下:

  1. 遍历 effectdeps 数组,依次从每个 Dep 实例中删除当前 effect
  2. 清空 effectdeps 数组。

cleanupEffect 函数在以下场景会被调用:

  • effect.run() 方法执行之前。
  • effect.stop() 方法执行时。

5. stop 函数:优雅的停止

stop 函数用于停止 effect 的执行,并清理所有相关的依赖。 它的核心逻辑如下:

stop() {
  if (this.active) {
    cleanupEffect(this); // 清理所有依赖
    if (this.options.onStop) {
      this.options.onStop(); // 执行 stop 回调
    }
    this.active = false; // 标记为停止状态
  }
}

stop 函数的执行流程如下:

  1. 检查 effect 是否处于激活状态。 只有激活状态的 effect 才能被停止。
  2. 调用 cleanupEffect 函数,清理所有相关的依赖。
  3. 如果 effect 存在 onStop 回调函数,则执行该回调函数。
  4. effectactive 属性设置为 false,表示该 effect 已经停止。

案例分析:一个简单的计数器

为了更好地理解 ReactiveEffect 的工作原理,我们来看一个简单的计数器案例:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    const count = ref(0);

    const increment = () => {
      count.value++;
    };

    onMounted(() => {
      console.log('Component mounted');
    });

    onUnmounted(() => {
      console.log('Component unmounted');
    });

    return {
      count,
      increment,
    };
  },
};
</script>

在这个案例中,count 是一个响应式数据。 当我们点击 "Increment" 按钮时,count.value 会被更新,从而触发视图的更新。

让我们来分析一下在这个案例中 ReactiveEffect 是如何工作的:

  1. 当组件渲染时,Vue 会创建一个 ReactiveEffect 实例,并将组件的渲染函数作为 fn 属性传入。
  2. 在渲染函数执行过程中,会访问 count.value,从而触发 countget 操作。
  3. get 操作中,track 函数会被调用,将当前的 ReactiveEffect 实例添加到 count 对应的 Dep 实例中。
  4. 当我们点击 "Increment" 按钮时,count.value 会被更新,从而触发 countset 操作。
  5. set 操作中,trigger 函数会被调用,触发 count 对应的 Dep 实例中的所有 effect
  6. ReactiveEffect 实例的 run 方法会被执行,重新执行组件的渲染函数,从而更新视图。

当组件卸载时,Vue 会调用 ReactiveEffect 实例的 stop 方法,清理所有相关的依赖,防止内存泄漏。

ReactiveEffect、Dep、targetMap的关系

我们可以用一张表格来总结 ReactiveEffectDeptargetMap 之间的关系:

组件 作用 关键属性/方法
ReactiveEffect 将依赖和副作用函数连接起来,负责追踪依赖和触发更新。 active: 标志 effect 是否处于激活状态;deps: 存储依赖的 Dep 实例;fn: 副作用函数;run(): 执行副作用函数;stop(): 停止 effect 的执行;cleanupEffect(): 清理依赖。
Dep 存储依赖于某个响应式数据的所有 effect add(effect: ReactiveEffect): 添加 effect;delete(effect: ReactiveEffect): 删除 effect;has(effect: ReactiveEffect): 是否包含 effect。
targetMap 存储所有响应式数据的依赖关系。 get(target: object): 获取 target 对应的 depsMap;set(target: object, depsMap: Map): 设置 target 对应的 depsMap。

总结

ReactiveEffect 类是 Vue 3 响应式系统的核心组件,它负责将依赖和副作用函数连接起来,形成一个高效的“数据-视图”同步机制。 通过 track 函数建立依赖关系,通过 trigger 函数触发更新,通过 cleanupEffect 函数清理依赖,通过 stop 函数停止 effect 的执行。 理解 ReactiveEffect 的工作原理,可以帮助我们更好地理解 Vue 3 响应式系统的底层机制,从而更好地使用 Vue 3 进行开发。

好了,今天的 Vue 3 源码解剖就到这里。 希望大家通过今天的学习,能够对 ReactiveEffect 有更深入的理解。 感谢大家的观看,我们下期再见!

发表回复

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