Vue 3源码极客之:`Vue`的`reactive`系统:`weakMap`和`Map`在依赖追踪中的作用。

各位老铁,晚上好!欢迎来到今晚的Vue 3源码扒皮现场。今天咱们要聊的是Vue 3响应式系统的核心 – reactive,以及其中扮演重要角色的 WeakMapMap。 准备好了吗? Let’s dive in!

第一幕:响应式系统的"前世今生"

在开始之前,先简单回顾一下啥是响应式。简单来说,就是当你的数据发生变化的时候,UI也能自动更新。这就像一个非常听话的仆人,主人(数据)一有啥风吹草动,他就立马行动(更新UI)。

Vue 2用的是 Object.defineProperty 来实现响应式,这玩意儿虽然好用,但有个致命的缺点:它只能监听对象的属性,而不能监听对象新增或删除属性,也不能监听数组的变化。 所以 Vue 2 为了解决数组的监听问题,搞了一堆hack方法,比如重写数组的 pushpop 等方法。

Vue 3 直接抛弃了 Object.defineProperty,拥抱了 ProxyProxy 就像一个代理人,拦截你对对象的所有操作,包括读取、设置、删除属性等等。这样一来,Vue 3 就能更优雅、更全面地实现响应式了。

第二幕:reactive函数的真面目

reactive 函数是 Vue 3 响应式系统的入口。它的作用就是把一个普通对象转换成响应式对象。 简单来说,就是给对象套上一层"魔法外衣",让它变得敏感起来,一旦数据发生变化,就能被感知到。

咱们先来看看 reactive 函数的简化版代码(注意,这只是简化版,真正的源码要复杂得多):

// 存储原始对象和代理对象的映射关系
const toProxy = new WeakMap();
const toRaw = new WeakMap();

function reactive(target) {
  // 如果已经代理过了,直接返回代理对象
  if (toProxy.has(target)) {
    return toProxy.get(target);
  }

  // 如果已经是响应式对象,直接返回
  if (toRaw.has(target)) {
    return target;
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (result && oldValue !== value) {
        // 触发更新
        trigger(target, key);
      }
      return result;
    }
  });

  // 建立原始对象和代理对象的映射关系
  toProxy.set(target, proxy);
  toRaw.set(proxy, target);

  return proxy;
}

这段代码的核心在于 ProxyWeakMap

  • Proxy 就像一个门卫,拦截对目标对象的所有操作。get 拦截读取属性的操作,set 拦截设置属性的操作。
  • WeakMap 用来存储原始对象和代理对象之间的映射关系。toProxy 存储原始对象到代理对象的映射,toRaw 存储代理对象到原始对象的映射。

第三幕:WeakMapMap的爱恨情仇

现在咱们来重点聊聊 WeakMapMap 在响应式系统中的作用。

  • WeakMap 的优势:

    • 避免内存泄漏: WeakMap 的键是弱引用,这意味着如果原始对象被垃圾回收器回收了,WeakMap 中对应的键值对也会被自动移除。这可以有效避免内存泄漏。
    • 性能优化: 由于 WeakMap 的键是弱引用,所以它的查找速度比 Map 更快。
  • Map 的优势:

    • 键的类型更灵活: Map 的键可以是任意类型,而 WeakMap 的键只能是对象。
    • 可以遍历: Map 可以通过 forEach 方法或者 for...of 循环来遍历,而 WeakMap 不能。

为什么 Vue 3 使用 WeakMap 来存储原始对象和代理对象的映射关系?

主要原因就是为了避免内存泄漏。想象一下,如果 Vue 3 使用 Map 来存储原始对象和代理对象的映射关系,那么即使原始对象不再被使用了,Map 中仍然会保存对原始对象的引用,导致原始对象无法被垃圾回收器回收,从而造成内存泄漏。

什么时候可以使用 Map

在 Vue 3 响应式系统中,Map 也有它的用武之地。比如,在 effect 函数中,我们需要存储 effect 函数和它所依赖的 reactive 对象的属性之间的关系。 这个时候,就可以使用 Map 来存储这种关系。

咱们先来看看 effect 函数的简化版代码:

let activeEffect = null;

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;
    fn();
    activeEffect = null;
  };
  effectFn();
}

const targetMap = new Map();

function track(target, key) {
  if (!activeEffect) return;

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }

  deps.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const deps = depsMap.get(key);
  if (!deps) return;

  deps.forEach(effectFn => {
    effectFn();
  });
}

这段代码的核心在于 targetMap 这个 Map

  • targetMap 存储 reactive 对象和它所依赖的属性之间的关系。targetMap 的键是 reactive 对象,值是一个 depsMapdepsMap 的键是属性名,值是一个 Set,存储依赖该属性的 effect 函数。

为什么这里使用 Map 而不是 WeakMap

因为 effect 函数需要主动管理依赖关系。当 effect 函数不再需要的时候,我们需要手动清除它和它所依赖的 reactive 对象之间的关系。 如果使用 WeakMap,就无法实现这种手动管理。

第四幕:代码示例,加深理解

为了更好地理解 WeakMapMap 在响应式系统中的作用,咱们来看一个简单的例子:

const obj = reactive({
  name: '张三',
  age: 18
});

effect(() => {
  console.log('name:', obj.name);
});

effect(() => {
  console.log('age:', obj.age);
});

obj.name = '李四'; // 输出:name: 李四
obj.age = 20;   // 输出:age: 20

在这个例子中,我们创建了一个响应式对象 obj,并定义了两个 effect 函数。 当 obj.nameobj.age 发生变化时,对应的 effect 函数就会被重新执行。

咱们来分析一下这段代码的执行过程:

  1. 创建响应式对象 obj reactive 函数会创建一个 Proxy 对象,并使用 WeakMap 存储原始对象和代理对象之间的映射关系。
  2. 执行第一个 effect 函数: effect 函数会把自身注册为 activeEffect,然后执行回调函数。在回调函数中,会读取 obj.name 属性。
  3. track 函数被调用: track 函数会把 activeEffect 添加到 targetMap 中,建立 objname 属性之间的依赖关系。
  4. 执行第二个 effect 函数: 和执行第一个 effect 函数类似,track 函数会把 activeEffect 添加到 targetMap 中,建立 objage 属性之间的依赖关系。
  5. 修改 obj.name 属性: Proxyset 拦截器会被调用,trigger 函数会被执行。
  6. trigger 函数找到所有依赖 obj.nameeffect 函数: trigger 函数会从 targetMap 中找到所有依赖 obj.name 属性的 effect 函数,并执行它们。
  7. 修改 obj.age 属性: 和修改 obj.name 属性类似,trigger 函数会找到所有依赖 obj.age 属性的 effect 函数,并执行它们。

第五幕:总结与升华

通过今天的扒皮,相信大家对 WeakMapMap 在 Vue 3 响应式系统中的作用有了更深入的理解。 简单总结一下:

数据结构 应用场景 优点 缺点
WeakMap 存储原始对象和代理对象的映射关系 避免内存泄漏,性能优化 键只能是对象,不能遍历
Map 存储 reactive 对象和依赖属性的关系 键的类型更灵活,可以遍历,方便手动管理依赖关系 如果不手动清除引用,可能会导致内存泄漏

记住,技术选型没有绝对的对错,只有适不适合。 在 Vue 3 响应式系统中,WeakMapMap 各司其职,共同构建了一个高效、稳定的响应式系统。

好了,今天的讲座就到这里。希望大家有所收获。 如果有什么疑问,欢迎在评论区留言。 咱们下期再见! (手动比心)

发表回复

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