Vue3中的Proxy

上篇文章中已经了解了使用Proxy进行数据代理时存在2个比较棘手的问题,而且vue3很好地解决了这些问题。不过,除了这些,Proxy还有一些其它的问题。

不能拦截SetMapWeakMapWeakSet

在vue3的源码中,也会发现对这些数据对象单独做了判断,不同的数据使用不同的handlers

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])

const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers

为什么要这样做?就是因为Proxy不能直接拦截SetMapWeakMapWeakSet这些类型的数据,如下示例

const map = new Proxy(new Map([['name', 'leevare']]), {
  get(target, p, receiver) {
    console.log('get');
    return Reflect.get(target, p, receiver);
  },
  set(target, p, value, receiver) {
    console.log('set');
    return Reflect.set(target, p, value, receiver);
  },
});
console.log(map.set('age', 1));

运行后报错

TypeError: Method Map.prototype.set called on incompatible receiver [object Object]

网上查了一下,是由于Internal slots这个东西导致的,Internal slots是个什么玩意儿呢?

所谓Internal slots,就是说MapWeakMap等等这些对象会将数据保存在Internal slots(内部插槽)中,所以只有MapWeakMap等这些数据对象在内部可以通过this访问到数据,但是代理Proxy是不存在这样的插槽的,当Map等这些数据对象被Proxy包装了之后,this就变成了Proxy对象,所以this自然就访问不到数据了。

解决办法是将this强制更正为原始数据对象。

// 代码来自:https://javascript.info/proxy#proxy-limitations
let map = new Map();

let proxy = new Proxy(map, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)

如果看过vue3的源码,你会发现对于Map等这些数据类型拦截处理它只有get,并没有set

export const mutableCollectionHandlers: ProxyHandler<any> = {
  get: createInstrumentationGetter(mutableInstrumentations)
}

export const readonlyCollectionHandlers: ProxyHandler<any> = {
  get: createInstrumentationGetter(readonlyInstrumentations)
}

这两个Handlers分别对应可变数据类型和只读数据类型。这里有一个createInstrumentationGetter方法,它负责返回一个Proxy中的get函数。

function createInstrumentationGetter(instrumentations: any) {
  return function getInstrumented(
    target: any,
    key: string | symbol,
    receiver: any
  ) {
    target =
      hasOwn(instrumentations, key) && key in target ? instrumentations : target
    return Reflect.get(target, key, receiver)
  }
}

只存在get的话,怎么能够做到拦截set实现数据响应式呢?在createInstrumentationGetter可以看到返回了一个getInstrumented函数,它对target做了一些赋值。

target = hasOwn(instrumentations, key) && key in target ? instrumentations : target

表示instrumentations中要存在key,并且target中也要包含key这个对象,如果包含这些,就返回instrumentations,否则返回原来的值。具体是代表什么意思,继续看代码。

通读一遍collectionHandlers.ts的代码,发现它是自己实现了getset等方法,也就是说,你在Map上调用的set其实是vue3自己封装的set方法,只是它在set中对数据进行一些拦截。

function set(this: any, key: any, value: any) {
  value = toRaw(value)
  const target = toRaw(this)
  // 这里的this其实就是Proxy后的Map对象
  const proto: any = Reflect.getPrototypeOf(this)
  const hadKey = proto.has.call(target, key)
  const oldValue = proto.get.call(target, key)
  // 执行Map原型上的set方法完成数据插入
  const result = proto.set.call(target, key, value)
  if (value !== oldValue) {
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key)
      } else {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }
  return result
}

到这里知道了,target = hasOwn(instrumentations, key) && key in target ? instrumentations : target就是判断调用的方法是否在对应的对象上,并且在vue3的内部也实现了这些方法,如果实现了的话,就将自己实现的方法对象集合赋值到原来的target上。

const map = reactive(new Map());
map.set('name', 'leevare');

也就是说在map调用set的时候,vue3先会检测内部是否实现了set方法,如果实现了,就使用内部的set方法。按照同样的思路,其内部实现了gethasaddsetdeleteclear方法,分别对应着不同的数据对象。

依照vue3的这种实现思路,可以模拟实现一个简单的Map对象拦截。

const reactToRaw = new WeakMap();
const rawToReact = new WeakMap();

function get(key) {
  console.log('get');
  // 这里要取到原始数据,否则会调用出错
  const target = reactToRaw.get(this) || this;
  const proto = Reflect.getPrototypeOf(target);
  return proto.get.call(target, key);
}

function set(key, value) {
  console.log('set');
  // 这里要取到原始数据,否则会调用出错
  const target = reactToRaw.get(this) || this;
  const proto = Reflect.getPrototypeOf(target);
  return proto.set.call(target, key, value);
}

const instrumentations = {
  get,
  set,
};

const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator];
iteratorMethods.forEach(method => {
  instrumentations[method] = function() {};
});

const handler = {
  get: function(target, key, receiver) {
    target = Object.prototype.hasOwnProperty.call(instrumentations, key) && key in target ? instrumentations : target;
    return Reflect.get(target, key, receiver);
  },
};

function reactive(value) {
  const result = new Proxy(value, handler);
  reactToRaw.set(result, value);
  rawToReact.set(value, result);
  return result;
}

测试一下效果

const map = new Map([['name', 'leevare']]);
const p = reactive(map);
p.set('age', 12);
console.log(p.get('age'));
console.log(p.get('name'));

结果输出

set
get
12
get
leevare

一切正常!

总结

Proxy虽是好东西,但仍然有很多坑要踩,所以,继续折腾!

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

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