Vue 3源码极客之:`Vue`的`reactive`:它如何处理`Set`、`Map`等集合类型的响应式。

各位靓仔靓女,晚上好!我是今天的主讲人,咱们今晚聊聊Vue 3 reactive里的那些“集合大佬”:Set、Map等等。

咳咳,开始之前先声明,今天咱不搞玄学,直接扒源码,争取用最通俗易懂的语言,把Vue 3 reactive处理集合类型的逻辑给各位安排明白。

开场白:为啥集合类型需要特别关照?

在Vue 3的世界里,reactive的核心任务就是把一个普通的JavaScript对象变成响应式的,一旦这个对象的属性发生改变,所有依赖于这个属性的视图或者计算属性都要跟着更新。

对于普通对象,这事儿很简单,直接用Proxy拦截getset等操作就完事了。但是,对于SetMap这种集合类型,情况就复杂多了。

为啥呢?因为集合类型有自己的专属操作方法,比如adddeleteclear,如果我们只拦截getset,那这些集合方法的操作就绕过了我们的监听,导致视图无法更新。

举个栗子:

const reactiveSet = reactive(new Set());

reactiveSet.add(1); // 视图没更新!

所以,Vue 3必须对这些集合类型进行特殊处理,才能保证它们的响应式特性。

第一幕:mutableCollectionHandlers——集合类型的“御用Handler”

Vue 3为了应对集合类型的特殊性,专门定义了一个mutableCollectionHandlers对象,里面包含了处理SetMap等可变集合类型的get拦截器。

// packages/reactivity/src/reactive.ts

export const mutableCollectionHandlers: ProxyHandler<any> = {
  get: /*#__PURE__*/ createCollectionGetter(),
  set: mutableCollectionSetter,
  has: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
  add: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
  delete: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
  clear: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
  forEach: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
  size: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
  //... 其他方法
};

可以看到,mutableCollectionHandlers里定义了getsethasadddeleteclearforEachsize等方法,这些方法都是为了拦截集合类型的各种操作。

第二幕:createCollectionGetter——“Getter制造机”

createCollectionGetter函数的作用是生成一个用于拦截集合类型get操作的getter函数。它的核心逻辑是:

  1. 拦截集合类型的get操作。
  2. 判断key是否是集合类型的方法(比如adddelete)。
  3. 如果是集合类型的方法,则返回一个经过特殊处理的函数,这个函数会在执行集合方法前后触发依赖收集和触发更新。
  4. 如果不是集合类型的方法,则直接返回原始值。
// packages/reactivity/src/reactive.ts

function createCollectionGetter(readonly: boolean = false, shallow: boolean = false, isReadonly: boolean = false) {
  return function get(target: any, key: string | symbol, receiver: any) {
    // 1. 拦截集合类型的get操作。
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return readonly;
    } else if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
      return target;
    }

    const targetIsReadonly = isReadonly || readonly;
    // target === Readonly + NOT Reactive
    // result === Readonly
    const self = targetIsReadonly ? target : target[RAW];
    const proxy = reactiveMap.get(self);

    if (proxy && proxy[key] !== void 0) {
      return proxy[key];
    }

    const getter = Reflect.get(target, key, receiver);

    // 2. 判断`key`是否是集合类型的方法(比如`add`、`delete`)。
    if (
      isSymbol(key) ||
      key === 'size' ||
      OWN_KEYS.has(key)
    ) {
      return getter;
    }

    if (!isReadonly) {
      track(
        self,
        TrackOpTypes.GET,
        key
      );
    }

    // 3. 如果是集合类型的方法,则返回一个经过特殊处理的函数
    if (isMap(target) && isMapIterationKey(key)) {
      // 返回一个迭代器
      return (...args: any[]) => {
        // 设置 readonly,禁止修改
        pauseTracking();
        const result = getter.apply(target, args);
        resetTracking();
        return {
          next() {
            return {
              done: false,
              value: reactive(result.next().value),
            };
          },
          [Symbol.iterator]() {
            return this;
          },
        };
      };
    }

    return isFunction(getter)
      ? getter.bind(self)
      : getter;
  };
}

第三幕:mutableCollectionSetter——集合类型的“Setter卫士”

mutableCollectionSetter函数的作用是拦截集合类型的set操作,并在set操作前后触发依赖收集和触发更新。

// packages/reactivity/src/reactive.ts
function mutableCollectionSetter(target: any, key: string | symbol, value: any, receiver: any) {
  // 1. 先执行原始的set操作。
  const result = Reflect.set(target, key, value, receiver);

  // 2. 触发依赖更新。
  trigger(target, TriggerOpTypes.SET, key, value);

  return result;
}

第四幕:实战演练——reactive(new Set())的内部流程

现在,我们来模拟一下reactive(new Set())的内部流程,看看Vue 3是如何把一个Set变成响应式的。

  1. 创建Set实例: 首先,我们创建一个Set的实例。

    const rawSet = new Set();
  2. 调用reactive函数: 然后,我们调用reactive函数,把rawSet变成响应式的。

    const reactiveSet = reactive(rawSet);
  3. 创建Proxy实例: reactive函数会创建一个Proxy实例,用于拦截rawSet的各种操作。

    const proxy = new Proxy(rawSet, mutableCollectionHandlers);
  4. 拦截add操作: 当我们调用reactiveSet.add(1)时,Proxy会拦截add操作,并执行mutableCollectionHandlers.get方法。

    reactiveSet.add(1); // 触发 Proxy 的 get 拦截
  5. createCollectionGetter发挥作用: mutableCollectionHandlers.get方法会调用createCollectionGetter函数,生成一个用于拦截add操作的getter函数。

  6. 执行add操作: createCollectionGetter返回的getter函数会先触发依赖收集,然后执行原始的add操作,最后触发依赖更新。

    // 触发依赖收集
    track(rawSet, TrackOpTypes.GET, 'add');
    
    // 执行原始的add操作
    rawSet.add(1);
    
    // 触发依赖更新
    trigger(rawSet, TriggerOpTypes.ADD, 1);
  7. 视图更新: 依赖更新会通知所有依赖于reactiveSet的视图或者计算属性进行更新,从而保证视图的响应式。

第五幕:readonlyshallowReactive的集合类型处理

除了reactive,Vue 3还提供了readonlyshallowReactive两个函数,用于创建只读的和浅层响应式的对象。

对于集合类型,readonlyshallowReactive的处理方式略有不同。

  • readonly readonly会创建一个只读的集合类型,任何尝试修改集合类型的操作都会抛出一个错误。

  • shallowReactive shallowReactive只会对集合类型的第一层属性进行响应式处理,如果集合类型的元素是对象,那么这些对象不会被递归地转换为响应式对象。

集合类型的readonly处理

// packages/reactivity/src/reactive.ts

export const readonlyCollectionHandlers: ProxyHandler<any> = {
  get: /*#__PURE__*/ createCollectionGetter(true),
  set(target: any, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true
  },
  add(target: any, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true
  },
  delete(target: any, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true
  },
  clear(target: any, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true
  },
};

总结

特性 reactive readonly shallowReactive
响应式深度 深层响应式,集合内部的元素如果是对象也会被递归转换为响应式对象 深层只读,集合内部的元素如果是对象也会被递归转换为只读对象 浅层响应式,集合内部的元素如果是对象则不会被转换为响应式对象
修改集合 允许修改集合,修改会触发视图更新 禁止修改集合,尝试修改会抛出警告(开发环境) 允许修改集合,修改会触发视图更新(仅限第一层属性)
应用场景 需要深度响应式且允许修改的场景,例如需要响应式更新的购物车数据、需要响应式更新的用户信息列表等 需要只读数据的场景,例如只读的配置信息、只读的用户权限列表等 只需要浅层响应式且允许修改的场景,例如只需要响应式更新的表单数据,但表单内部的对象不需要响应式更新等
性能考虑 性能开销较大,因为需要递归地转换对象为响应式对象 性能开销较大,因为需要递归地转换对象为只读对象,并且需要进行额外的只读检查 性能开销较小,因为只需要转换第一层属性为响应式对象,无需递归转换
适用集合类型 SetMapWeakSetWeakMap SetMapWeakSetWeakMap SetMapWeakSetWeakMap

最后,来个小彩蛋

Vue 3 源码里还对 WeakSetWeakMap 做了特殊处理,因为它们是弱引用集合,垃圾回收机制可能会回收掉集合里的元素,导致响应式失效。所以 Vue 3 对 WeakSetWeakMap 的响应式处理更加谨慎,这里就不展开讲了,有兴趣的同学可以自己去研究源码。

好了,今天的分享就到这里,希望各位靓仔靓女有所收获!下次再见!

发表回复

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