如何使用Proxy拦截Map和Set的操作

2,608 阅读3分钟

Proxy在代理普通对象的时候,只需要使用setget两个拦截函数就可以实现一些效果。

思考一下,我们在调用MapSet对象的时候,只会用到get拦截,如map.get('key')map.set('key','value)map.has('key'),实际传入的参数都是在Map对象的方法里接收的,想要达到拦截的目的,实际需要操作的是Map对象的get()set()这些方法。

函数拦截

我们需要对MapSet对象的方法进行拦截,在Proxy中的get拦截时,我们可以获取到调用的函数名。

// 自定义的拦截器,在这里对调用的Map方法进行拦截
const interceptors = {
  get(key) {
    console.log(key);
  },
  set(key, value) {
    console.log(key, value);
  },
};
// 创建代理
const map = new Proxy(new Map(), {
  get(target, key, receiver) {
    // 如果调用的key存在于我们自定义的拦截器里,就用我们的拦截器
    target = interceptors.hasOwnProperty(key) ? interceptors : target;
    return Reflect.get(target, key, receiver);
  },
});

map.set("key", "value"); // 输出 key value
map.get("key"); // 输出 key

根据输出可以看出,调用mapsetget方法都调用了我们自己的函数,下一步就需要在自己的函数里,去调用map对应的函数来实现拦截的功能且不影响原有功能。

如何在interceptors拦截器里获取当前调用的对象呢?Reflect.get的第三个参数,即是在调用时的this,所以我们在interceptors中访问的this指向receiver的值为经过Proxy后的map对象。

// 自定义的拦截器,在这里对调用的Map方法进行拦截
const interceptors = {
  get(key) {
    return this.get(key);
  },
  // ...
};

// ...

map.get("key");

然而如果直接在调用this.get是不行的,this指向Proxy后的对象,再通过此对象访问又会进入我们自定义的拦截器,就死循环了,所以我们需要在自定义对象中,通过this这个Proxy后的对象来找到原始的对象。

所以我们需要在创建Proxy对象时,把原始对象和Proxy对象之间建立一个映射,这样就可以通过Proxy对象找到原始对象了。

改写一下代码:

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

// 自定义的拦截器,在这里对调用的Map方法进行拦截
const interceptors = {
  get(key) {
    // 通过代理对象获取原始对象 (proxy => map)
    const rawTarget = proxyToRaw.get(this);
    return rawTarget.get(key);
  },
  set(key, value) {
    const rawTarget = proxyToRaw.get(this);
    return rawTarget.set(key, value);
  },
};

// 创建Proxy对象,同时将代理对象和原始对象建立一个映射
const createProxy = (obj) => {
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      // 如果调用的key存在于我们自定义的拦截器里,就用我们的拦截器
      target = interceptors.hasOwnProperty(key) ? interceptors : target;
      return Reflect.get(target, key, receiver);
    },
  });
  // 让proxy后的对象指向原始对象
  proxyToRaw.set(proxy, obj);
  return proxy;
};

const map = createProxy(new Map());

map.set("key", "value");
map.get("key"); // 输出value

通过代理对象到原始对象的映射,我们可以通过this来找到传入的原始对象,再调用原始对象上的方法达到正确的效果,getset的代理就完成了。

其他方法还有Set对象实现的方法也是同理。

this问题

const proxy = new Proxy(new Map(), {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});

proxy.get("key"); // TypeError: Method Map.prototype.get called on incompatible receiver

在什么操作都没有做的时候,直接返回Reflect.get(target, key, receiver),会出现这样一个错误,因为Map的方法都在原型链Map.prototype上,而值存在原对象内部槽[[MapData]]上,而在通过代理后的Proxy对象上没有[[MapData]],所以报错了,要解决这个问题,还是得修改this指向。

const proxy = new Proxy(new Map(), {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver).bind(target); // bind this
  },
});

这个问题是我的个人见解,可能不一定对,欢迎讨论。


本文参考:

  1. 带你彻底搞懂Vue3的Proxy响应式原理!基于函数劫持实现Map和Set的响应式
  2. observer-util