深入理解 Vue 3 源码中 `effectScope` 的实现,它如何利用 `Map` 关联 `effect` 实例,并在 `stop` 时进行高效清理?

各位听众,晚上好!欢迎来到今天的 Vue 3 源码剖析特别讲座。今天我们要聊的,是 Vue 3 响应式系统里一个非常重要的组成部分,但可能平时我们不太直接接触,容易忽略的家伙:effectScope

准备好了吗?让我们一起深入 effectScope 的世界,看看它到底是如何工作的,以及它在 Vue 3 的响应式系统中扮演着怎样的角色。

1. 什么是 effectScope?为什么需要它?

简单来说,effectScope 是一个用来管理一组 effect 的容器。它允许你将多个 effect 集中管理,然后统一控制它们的生命周期。你可能会问,为什么需要这么个东西呢?

想象一下,你在一个 Vue 组件里创建了多个 effect,比如监听多个响应式数据的变化,然后执行不同的操作。当组件卸载的时候,你必须手动 stop 这些 effect,否则它们可能会继续执行,导致内存泄漏或者出现一些奇怪的 bug。

如果没有 effectScope,你就需要记住所有创建的 effect,然后逐个 stop。这很繁琐,而且容易出错。

有了 effectScope,你就可以把这些 effect 都放到一个 scope 里,然后只需要 stop 这个 scope,就可以一次性 stop 所有相关的 effect。这大大简化了代码,提高了效率,也降低了出错的风险。

2. effectScope 的基本用法

在 Vue 3 中,你可以这样使用 effectScope

import { effectScope, ref, computed, onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const doubled = computed(() => count.value * 2);

    const scope = effectScope();

    scope.run(() => {
      // 创建 effect 1
      effect(() => {
        console.log('count changed:', count.value);
      });

      // 创建 effect 2
      effect(() => {
        console.log('doubled changed:', doubled.value);
      });
    });

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

    onUnmounted(() => {
      scope.stop(); // 组件卸载时,停止所有 effect
      console.log('Component unmounted');
    });

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

    return { count, doubled, increment };
  },
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <p>Doubled: {{ doubled }}</p>
      <button @click="increment">Increment</button>
    </div>
  `
};

在这个例子中,我们创建了一个 effectScope,然后在 scope.run() 中创建了两个 effect。当组件卸载时,我们调用 scope.stop(),就可以停止这两个 effect。是不是很简单?

3. 源码剖析:effectScope 的内部实现

好了,现在让我们深入 effectScope 的源码,看看它是如何实现的。

3.1 effectScope 的数据结构

effectScope 的核心数据结构主要包括以下几个部分:

  • active: 一个布尔值,表示 effectScope 是否处于激活状态。只有激活状态的 effectScope 才能收集 effect
  • effects: 一个数组,用来存储所有属于该 effectScopeeffect 实例。
  • cleanups: 一个数组,用来存储一些清理函数,这些函数会在 effectScopestop 的时候执行。
  • parent: 指向父 effectScope 的指针。effectScope 可以嵌套使用,形成一个树状结构。
  • scopes: 用于存储子 effectScope 的数组,形成嵌套关系。

用 TypeScript 描述如下:

interface EffectScope {
  active: boolean;
  effects: ReactiveEffect[]; // ReactiveEffect 是 effect 的实例类型,后续会讲到
  cleanups: (() => void)[];
  parent: EffectScope | undefined;
  scopes: EffectScope[] | undefined;
  run<T>(fn: () => T): T | undefined;
  stop(): void;
}

3.2 effectScope 的创建

effectScope 的创建非常简单,就是创建一个包含上述属性的对象。

export function effectScope(detached = false) {
  const scope: EffectScope = {
    active: true,
    effects: [],
    cleanups: [],
    parent: currentEffectScope, // 指向当前激活的 effectScope,用于嵌套
    scopes: undefined, // 用于存储子 scope
    run<T>(fn: () => T): T | undefined {
      if (!this.active) {
        return;
      }
      try {
        this.parent = currentEffectScope;
        currentEffectScope = this;
        return fn();
      } finally {
        currentEffectScope = this.parent;
      }
    },
    stop() {
      if (this.active) {
        let i, l;
        for (i = 0, l = this.effects.length; i < l; i++) {
          this.effects[i].stop();
        }
        for (i = 0, l = this.cleanups.length; i < l; i++) {
          this.cleanups[i]();
        }
        if (this.scopes) {
          for (i = 0, l = this.scopes.length; i < l; i++) {
            this.scopes[i].stop();
          }
        }
        this.active = false;
      }
    }
  };

  if (!detached && currentEffectScope) {
    // 如果不是 detached 且当前有 active scope,则将当前 scope 添加到父 scope 的 scopes 中
    (currentEffectScope.scopes || (currentEffectScope.scopes = [])).push(scope);
  }

  return scope;
}

这里需要注意的是 currentEffectScope 这个变量。它是一个全局变量,用来记录当前激活的 effectScope。当你在 scope.run() 中创建 effect 时,effect 就会被添加到 currentEffectScopeeffects 数组中。

3.3 scope.run() 的作用

scope.run() 的作用是执行一个函数,并将当前 effectScope 设置为激活状态。这样,在函数中创建的 effect 就会被自动添加到该 effectScope 中。

scope.run 的代码如下:

    run<T>(fn: () => T): T | undefined {
      if (!this.active) {
        return;
      }
      try {
        this.parent = currentEffectScope;
        currentEffectScope = this;
        return fn();
      } finally {
        currentEffectScope = this.parent;
      }
    },

可以看到,scope.run() 主要做了以下几件事:

  1. 检查 effectScope 是否处于激活状态。如果未激活,则直接返回。
  2. 保存当前的 currentEffectScopethis.parent,用于后续恢复。
  3. 将当前的 effectScope 设置为 currentEffectScope
  4. 执行传入的函数 fn
  5. finally 块中,将 currentEffectScope 恢复为之前的 parent

3.4 scope.stop() 的作用

scope.stop() 的作用是停止所有属于该 effectScopeeffect,并执行所有的清理函数。

scope.stop() 的代码如下:

    stop() {
      if (this.active) {
        let i, l;
        for (i = 0, l = this.effects.length; i < l; i++) {
          this.effects[i].stop();
        }
        for (i = 0, l = this.cleanups.length; i < l; i++) {
          this.cleanups[i]();
        }
        if (this.scopes) {
          for (i = 0, l = this.scopes.length; i < l; i++) {
            this.scopes[i].stop();
          }
        }
        this.active = false;
      }
    }

可以看到,scope.stop() 主要做了以下几件事:

  1. 检查 effectScope 是否处于激活状态。如果未激活,则直接返回。
  2. 遍历 effects 数组,调用每个 effectstop() 方法。
  3. 遍历 cleanups 数组,执行每个清理函数。
  4. 遍历 scopes 数组,递归调用每个子 effectScopestop() 方法。
  5. active 设置为 false,表示该 effectScope 已经被停止。

3.5 effect 如何关联到 effectScope

关键点在于 effect 函数内部会检查 currentEffectScope 是否存在,如果存在,则将当前 effect 实例添加到 currentEffectScope.effects 数组中。 让我们简单看一下effect函数的简化版本, 重点关注关联effectScope的部分:

function effect(fn, options:any = {}) {
  const effect = createReactiveEffect(fn, options);
  if (!options.lazy) {
    effect();
  }
  return effect;
}

function createReactiveEffect(fn, options:any) {
  const effect = function reactiveEffect(...args) {
    try {
      activeEffect = effect; // 设置当前激活的 effect
      return fn(...args); // 执行 effect 函数
    } finally {
      activeEffect = undefined; // 清空当前激活的 effect
    }
  }

  //... 其他代码

  if(currentEffectScope && currentEffectScope.active){
        currentEffectScope.effects.push(effect)
  }

  return effect
}

createReactiveEffect 函数中,可以看到,如果 currentEffectScope 存在且处于激活状态,那么就会将新创建的 effect 实例添加到 currentEffectScope.effects 数组中。 activeEffect 的作用是跟踪当前正在执行的 effect,而 currentEffectScope 则跟踪当前激活的 effect scope。 它们是两个不同的概念,但都依赖于全局状态来跟踪当前上下文。

4. Map 的使用?在哪里?

你可能会问,我们不是要讲 Map 吗?为什么到现在还没看到 Map 的身影?

实际上,effectScope 并没有直接使用 Map 来关联 effect 实例。它使用的是数组 effects 来存储 effect 实例。 但是,Vue 3 的响应式系统中,Map 的使用非常广泛,比如在 WeakMap 中用于存储 targetdepsMap 的映射,以及在 depsMap 中用于存储 keySet<ReactiveEffect> 的映射。

虽然 effectScope 没有直接使用 Map,但是它和 effect 实例的关系,与 WeakMaptargetdepsMap 的关系,以及 depsMapkeySet<ReactiveEffect> 的关系,本质上是一样的:都是一种一对多的关系。

effectScope 使用数组来存储 effect 实例,主要是因为它只需要在 stop 的时候遍历所有 effect 实例,然后调用它们的 stop() 方法。使用数组可以简化代码,提高效率。

如果 effectScope 需要频繁地查找某个特定的 effect 实例,那么使用 Map 可能会更合适。但是,在 Vue 3 的场景下,effectScope 并不需要这样做,所以使用数组就足够了。

5. stop 时的高效清理

effectScopestop 时的高效清理,主要体现在以下几个方面:

  • 遍历数组: effectScope 使用数组来存储 effect 实例,所以在 stop 的时候,只需要遍历数组,调用每个 effect 实例的 stop() 方法。数组的遍历效率很高,可以快速地停止所有相关的 effect
  • 清理函数: effectScope 允许你注册一些清理函数,这些函数会在 stop 的时候执行。你可以使用这些清理函数来释放一些资源,比如取消订阅事件,或者清理一些定时器。
  • 递归停止: effectScope 可以嵌套使用,形成一个树状结构。在 stop 的时候,effectScope 会递归地停止所有的子 effectScope。这样,你只需要 stop 最顶层的 effectScope,就可以停止所有相关的 effect
  • setActive标志位 active 标志位的存在可以避免已经停止的 effectScope 被重复 stop,从而提高效率。

6. detachedeffectScope

effectScope 还有一个 detached 的选项。如果设置为 true,那么创建的 effectScope 就不会自动添加到当前的 currentEffectScope 中。这意味着你需要手动管理这个 effectScope 的生命周期。

detachedeffectScope 主要用于一些特殊的场景,比如你需要创建一个独立的 effectScope,不受父 effectScope 的影响。

7. 总结

effectScope 是 Vue 3 响应式系统中的一个重要组成部分。它允许你将多个 effect 集中管理,然后统一控制它们的生命周期。effectScope 的实现非常简洁高效,它使用数组来存储 effect 实例,并在 stop 的时候遍历数组,调用每个 effect 实例的 stop() 方法。

特性 描述
目的 管理一组 effect,统一控制它们的生命周期。
数据结构 active (boolean), effects (ReactiveEffect[]), cleanups (() => void)[], parent (EffectScope), scopes (EffectScope[])
scope.run() 执行函数,将当前 effectScope 设置为激活状态,从而使在函数中创建的 effect 被自动添加到该 effectScope 中。
scope.stop() 停止所有属于该 effectScopeeffect,执行所有的清理函数,并递归停止所有子 effectScope
关联方式 effect 创建时检查 currentEffectScope,如果存在,则将 effect 实例添加到 currentEffectScope.effects 数组中。
高效清理 遍历数组停止 effect,执行清理函数,递归停止子 effectScope,使用 active 标志位避免重复停止。
detached 创建独立的 effectScope,不受父 effectScope 的影响,需要手动管理生命周期。
Map 的关系 虽然 effectScope 没有直接使用 Map,但其 effectScopeeffect 的关系与响应式系统中 WeakMaptargetdepsMap 的关系类似,都是一种一对多的关系。

希望通过今天的讲座,你对 effectScope 有了更深入的理解。掌握 effectScope 的使用,可以帮助你编写更健壮、更高效的 Vue 3 应用。

感谢各位的聆听!下次再见!

发表回复

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