Vue3源码reactivity响应式篇之Map、Set等代理处理详解

概览

vue3中实现集合对象(Map/WeakMap/Set/WeakSet)的处理器方法,也是针对四种集合对象的代理方法,如:响应式集合对象、浅层响应式集合对象、只读集合对象和浅层只读集合对象。但是集合对象处理器方法没有使用class的集成实现,具体参见packages\reactivity\src\collectionHandlers.ts

源码分析

首先要理解集合对象的代理方法就是一个包含get方法的对象,如下所示:

js 复制代码
const mutableCollectionHandlers = {
  get: /* @__PURE__ */ createInstrumentationGetter(false, false)
};
const shallowCollectionHandlers = {
  get: /* @__PURE__ */ createInstrumentationGetter(false, true)
};
const readonlyCollectionHandlers = {
  get: /* @__PURE__ */ createInstrumentationGetter(true, false)
};
const shallowReadonlyCollectionHandlers = {
  get: /* @__PURE__ */ createInstrumentationGetter(true, true)
};

createInstrumentationGetter

createInstrumentationGetter方法就是用于创建getter方法,接受两个参数:isReadonly(是否只读)和shallow(是否是浅层响应)。

createInstrumentationGetter的源码实现如下:

js 复制代码
function createInstrumentationGetter(isReadonly2, shallow) {
  const instrumentations = createInstrumentations(isReadonly2, shallow);
  return (target, key, receiver) => {
    if (key === "__v_isReactive") {
      return !isReadonly2;
    } else if (key === "__v_isReadonly") {
      return isReadonly2;
    } else if (key === "__v_raw") {
      return target;
    }
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target ? instrumentations : target,
      key,
      receiver
    );
  };
}

createInstrumentationGetter方法也是一个高阶函数,它返回一个getter方法。首先会调用createInstrumentations获取一个对象instrumentations,该对象内部就是包含vue3针对集合对象Map/WeakMap/Set/WeakSet重写的一些实例(静态)方法;然后返回一个gettergetter内部首先会先判断key值是否是__v_isReactive__v_isReadonly或者是__v_raw,返回值由参数isReadonly2target决定;若key不是这三者之一,则调用Reflect.get,若keyinstrumentations中重写实现的方法名并且也是集合对象的原生方法,则Reflect.get的第一个参数是instrumentations,否则为target

createInstrumentations

createInstrumentations方法的源码实现如下:

js 复制代码
function createInstrumentations(readonly, shallow) {
  const instrumentations = {
    get(key) {
      const target = this["__v_raw"];
      const rawTarget = toRaw(target);
      const rawKey = toRaw(key);
      if (!readonly) {
        if (hasChanged(key, rawKey)) {
          track(rawTarget, "get", key);
        }
        track(rawTarget, "get", rawKey);
      }
      const { has } = getProto(rawTarget);
      const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive;
      if (has.call(rawTarget, key)) {
        return wrap(target.get(key));
      } else if (has.call(rawTarget, rawKey)) {
        return wrap(target.get(rawKey));
      } else if (target !== rawTarget) {
        target.get(key);
      }
    },
    get size() {
      const target = this["__v_raw"];
      !readonly && track(toRaw(target), "iterate", ITERATE_KEY);
      return Reflect.get(target, "size", target);
    },
    has(key) {
      const target = this["__v_raw"];
      const rawTarget = toRaw(target);
      const rawKey = toRaw(key);
      if (!readonly) {
        if (hasChanged(key, rawKey)) {
          track(rawTarget, "has", key);
        }
        track(rawTarget, "has", rawKey);
      }
      return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey);
    },
    forEach(callback, thisArg) {
      const observed = this;
      const target = observed["__v_raw"];
      const rawTarget = toRaw(target);
      const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive;
      !readonly && track(rawTarget, "iterate", ITERATE_KEY);
      return target.forEach((value, key) => {
        return callback.call(thisArg, wrap(value), wrap(key), observed);
      });
    }
  };
  extend(
    instrumentations,
    readonly ? {
      add: createReadonlyMethod("add"),
      set: createReadonlyMethod("set"),
      delete: createReadonlyMethod("delete"),
      clear: createReadonlyMethod("clear")
    } : {
      add(value) {
        if (!shallow && !isShallow(value) && !isReadonly(value)) {
          value = toRaw(value);
        }
        const target = toRaw(this);
        const proto = getProto(target);
        const hadKey = proto.has.call(target, value);
        if (!hadKey) {
          target.add(value);
          trigger(target, "add", value, value);
        }
        return this;
      },
      set(key, value) {
        if (!shallow && !isShallow(value) && !isReadonly(value)) {
          value = toRaw(value);
        }
        const target = toRaw(this);
        const { has, get } = getProto(target);
        let hadKey = has.call(target, key);
        if (!hadKey) {
          key = toRaw(key);
          hadKey = has.call(target, key);
        } else if (!!(process.env.NODE_ENV !== "production")) {
          checkIdentityKeys(target, has, key);
        }
        const oldValue = get.call(target, key);
        target.set(key, value);
        if (!hadKey) {
          trigger(target, "add", key, value);
        } else if (hasChanged(value, oldValue)) {
          trigger(target, "set", key, value, oldValue);
        }
        return this;
      },
      delete(key) {
        const target = toRaw(this);
        const { has, get } = getProto(target);
        let hadKey = has.call(target, key);
        if (!hadKey) {
          key = toRaw(key);
          hadKey = has.call(target, key);
        } else if (!!(process.env.NODE_ENV !== "production")) {
          checkIdentityKeys(target, has, key);
        }
        const oldValue = get ? get.call(target, key) : void 0;
        const result = target.delete(key);
        if (hadKey) {
          trigger(target, "delete", key, void 0, oldValue);
        }
        return result;
      },
      clear() {
        const target = toRaw(this);
        const hadItems = target.size !== 0;
        const oldTarget = !!(process.env.NODE_ENV !== "production") ? isMap(target) ? new Map(target) : new Set(target) : void 0;
        const result = target.clear();
        if (hadItems) {
          trigger(
            target,
            "clear",
            void 0,
            void 0,
            oldTarget
          );
        }
        return result;
      }
    }
  );
  const iteratorMethods = [
    "keys",
    "values",
    "entries",
    Symbol.iterator
  ];
  iteratorMethods.forEach((method) => {
    instrumentations[method] = createIterableMethod(method, readonly, shallow);
  });
  return instrumentations;
}

相比之前的vue3源码,createInstrumentations简化了集合对象的重写,参数readonlyshallow表示是否是只读对象和是否是浅响应式对象,它们决定了返回的对象instrumentations中会包含哪些方法以及方法的具体实现。

先来回顾下集合对象分别有哪些方法:

方法 Map Set WeakMap WeakSet
.size
.set(key, value) - -
.get(key) - -
.has(key)
.delete(key)
.clear()
.add(value) - -
.keys()
.values()
.entries()
.forEach()
Symbol.iterator ✅ (同.entries) ✅ (同.values)

对比createInstrumentations方法,其内部就是定义了如上12种方法,如下:

  • get(key)
    get(key)方法是MapWeakMap实例的方法,用于获取指定键对应的值。
    get(key)接受一个key参数,表示键,首先通过this["__v_raw"]获取实例target,这里的this指向的就是Reflect.get的第三个参数receiver,即getter中第三个参数receiver表示代理对象。获取代理对象的实例target后,调用toRaw获取原始数据rawTarget以及原始键rawKey。然后判断readonly,若不是只读对象,则判断key是否是原始键rawKey,若不是,则为key调用track建立依赖收集;然后为rawKey调用track建立依赖。

    接着,用Reflect.getPrototypeOf获取原始实例rawTargethas方法;根据shallowreadonly来决定wrap装饰方法;若是浅层响应,则将toShallow赋值给wrap;否则判断readonly,若是深层响应只读,则wraptoReadonly;若是深层响应可写对象,则wraptoReactive。这样确保了最后获取的值和原始实例的响应式和只读特性保持一致。

    最后通过一些if...else获取值,若是target.has(key)存在,则返回wrap(target.get(key));否则若是rawTarget.has(rawKey)存在,则返回wrap(target.get(rawKey));否则最后判断targetrawTarget是否是同一对象,若是,则直接返回target.get(key)

  • **get size()
    size属性是MapSet实例的属性,用于获取集合的元素数量。
    get size()本质上是一个访问器属性方法,内部就是先获取代理对象的实例target,然后判断是否只读,若不是只读对象,则调用track建立依赖,当该代理对象被迭代时,就会触发响应的依赖(监听)。最后通过Reflect.get获取targetsize属性值,并返回。

  • has(key)
    MapSetWeakMapWeakSet均有has方法,用于判断集合是否包含指定的键或值。
    has(key)方法会先获取代理对象的实例target,然后通过toRaw获取原始对象rawTarget和原始键rawKey。判断readonly,若不是只读对象,则判断key是否是原始键rawKey即是否发生了改变,若不等,则为key调用track建立依赖收集;然后为rawKey调用track建立依赖。

    最后,判断key值是否是原始键rawKey,若是,则调用target.has(key)返回结果;若不是,则优先调用target.has(key),若为false,再调用target.has(rawKey)返回结果。

  • forEach(callback, thisArg)
    forEachMapSet的实例方法,用于遍历集合中的元素。接受一个函数callback参数和thisArg对象。
    forEach同样会先获取代理对象的实例target以及原始对象rawTarget,然后根据shallowreadonly获取装饰函数wrap。再根据是否只读,若不是只读对象,则调用track建立依赖收集。

    最后调用target.forEach遍历集合对象,执行callback方法,并且调用wrap包装键key和值key

addsetdeleteclear会改变代理对象的实例target,因此对于只读对象,不应该调用这些方法,vue3中在开发环境,当对只读集合对象调用者四个方法时,会打印警告信息,内部不会做其他操作。

  • add(value)
    add(value)SetWeakSet的方法,用于新增元素。
    add(value)方法接收参数value,若是深层响应式对象,并且value也是深层响应式且不是只读的,则会调用toRaw(value)获取参数的原始值并赋值给value。然后获取代理对象的实例target以及它的原型proto,调用原型的has方法判断target上是否存在相同的value,若不存在即hadKeyfalse,则调用target.add新增元素,再调用trigger触发target上与add相关的监听;最后返回this。这样确保了Set上的值都是唯一的,而且最后返回this,可以方便进行链式操作。

  • set(key, value)
    set(key,value)MapWeakMap的实例用于新增键值对的方法。

    add方法,set方法会对value进去去响应式处理,获取原始值value。然后获取代理对象的实例target,以及从它的原型上获取hasget方法。然后判断target上是否存在key属性,若不存在,则将key去响应式,继续调用has方法判断target上是否存在原始key属性,记为hadKey;调用原型上的get方法获取target上的key值记为旧值oldValue;调用target.set(key,value)新增键值对。然后判断hadKey值的布尔属性,若hadKeyfalse,则说明target上不存在key键和原始key键,这是一个新增操作,那么就会调用trigger触发add相关的监听;若hadKey存在,则说明是一个重新赋值的更新操作,判断旧值和新值是否相等,若不等,则调用trigger触发set相关的监听。最后返回this

  • delete(key)
    MapSetWeakMapWeakSet均有delete方法。对于MapWeakMap,该方法是删除指定键值对(即键为key);对于SetWeakSet,该方法是删除指定元素(key即为元素)。
    delete(key)方法会先获取代理对象的实例target以及其原型上的hasget方法。然后调用has方法来判断target上是否存在key属性(或键),若不存在,则获取key的原始值,判断target上是否存在原始key,记为hadKey;由前面我们知道get方法只有MapWeakMap的实例才有,因此若是SetWeakSet的实例,则不能通过get获得旧值oldValue;然后调用target.delete(key)删除元素结果记为result,然后判断hadKey的布尔属性,若存在,则调用trigger触发delete相关的依赖;最后返回result.

  • clear()
    clear()MapSet的实例方法,用于清空集合中的所有元素。
    clear()方法会先获取代理对象的实例target,然后读取对象的size判断是否为0;然后调用isMap判断当前targetMap实例还是Set实例,通过new构建新的实例记为oldTarget;然后调用target.clear()清空元素或者键值对,结果记为result。若当前target不是空对象,则调用trigger触发clear相关的监听,旧值是oldTarget

  • keys()values()entries()Symbol.iterator

    上述四种方法都是集合对象的迭代器方法,用于遍历集合中的元素,只有MapSet实例才有。vue3中是基于createIterableMethod重写了它们。

createIterableMethod

createIterableMethod顾名思义,就是用于创建可迭代的方法,接受三个参数:method(方法名)、isReadonly2(是否只读)、isShallow2(是否是浅层响应)。返回一个函数,该函数返回一个对象,实现了[Symbol.iterator]方法。其实现如下:

js 复制代码
function createIterableMethod(method, isReadonly2, isShallow2) {
    return function(...args) {
      const target = this["__v_raw"];
      const rawTarget = toRaw(target);
      const targetIsMap = isMap(rawTarget);
      const isPair = method === "entries" || method === Symbol.iterator && targetIsMap;
      const isKeyOnly = method === "keys" && targetIsMap;
      const innerIterator = target[method](...args);
      const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive;
      !isReadonly2 && track(
        rawTarget,
        "iterate",
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      );
      return {
        // iterator protocol
        next() {
          const { value, done } = innerIterator.next();
          return done ? { value, done } : {
            value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
            done
          };
        },
        // iterable protocol
        [Symbol.iterator]() {
          return this;
        }
      };
    };
  }

createIterableMethod返回函数中的this依旧是指向代理对象,获取代理对象的对象target以及原始对象rawTarget,调用isMap判断当前原始对象是否是Map实例;若method方法名是entries或者是Symbol.iterator且当前原始对象是Map的实例,则isPairtrue,否则为false。若方法名是keys且当前原始对象是Map实例,则isKeyOnlytrue

调用target[method](...args)获取对象默认的迭代器,记为innerIterator。根据是否是浅层响应以及是否只读确定装饰方法wrap;若不是只读对象,则调用track建立iterate相关的依赖收集。

最后的部分就是定义返回的对象,返回对象中包含两个方法:next()[Symbol.iterator]next中就是对默认的迭代器进行调用,获取valuedone,若donetrue,说明迭代完了,则直接返回{value,done};若donefalse,则判断是否是isPair,若isPairtrue,则说返回[wrap(value[0]), wrap(value[1])];否则直接返回wrap(value)。而[Symbol.iterator]内部就是返回this.

相关推荐
Tachyon.xue2 小时前
Vue 3 项目集成 Element Plus + Tailwind CSS 详细教程
前端·css·vue.js
FuckPatience3 小时前
Vue 中‘$‘符号含义
前端·javascript·vue.js
东风西巷5 小时前
K-Lite Mega/FULL Codec Pack(视频解码器)
前端·电脑·音视频·软件需求
超级大只老咪6 小时前
何为“类”?(Java基础语法)
java·开发语言·前端
你的人类朋友8 小时前
快速搭建redis环境并使用redis客户端进行连接测试
前端·redis·后端
这里是杨杨吖9 小时前
SpringBoot+Vue医院预约挂号系统 附带详细运行指导视频
vue.js·spring boot·医院·预约挂号
深蓝电商API9 小时前
实战破解前端渲染:当 Requests 无法获取数据时(Selenium/Playwright 入门)
前端·python·selenium·playwright
bestcxx9 小时前
(二十七)、k8s 部署前端项目
前端·容器·kubernetes
鲸落落丶10 小时前
webpack学习
前端·学习·webpack
excel10 小时前
深入理解 3D 火焰着色器:从 Shadertoy 到 Three.js 的完整实现解析
前端