Vue3中的Proxy
上篇文章中已经了解了使用Proxy进行数据代理时存在2个比较棘手的问题,而且vue3很好地解决了这些问题。不过,除了这些,Proxy
还有一些其它的问题。
不能拦截Set
、Map
、WeakMap
、WeakSet
在vue3的源码中,也会发现对这些数据对象单独做了判断,不同的数据使用不同的handlers
。
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
为什么要这样做?就是因为Proxy
不能直接拦截Set
、Map
、WeakMap
、WeakSet
这些类型的数据,如下示例
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,就是说Map
、WeakMap
等等这些对象会将数据保存在Internal slots(内部插槽)中,所以只有Map
、WeakMap
等这些数据对象在内部可以通过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的代码,发现它是自己实现了get
、set
等方法,也就是说,你在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
方法。按照同样的思路,其内部实现了get
、has
、add
、set
、delete
、clear
方法,分别对应着不同的数据对象。
依照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
虽是好东西,但仍然有很多坑要踩,所以,继续折腾!
- 本博客所有文章除特别声明外,均可转载和分享,转载请注明出处!
- 本文地址:https://www.leevii.com/?p=2506