Set和Map类型的数据有特定的属性和方法来操作自身,这点和普通对象十分不同。因此不能像代理普通对象那样代理Set和Map类型的数据。当然总体的思路还是一样,读取操作时,调用track建立响应联系;设置操作时,调用trigger触发响应。
那么在实现之前,有必要先了解使用Proxy代理Set以及Map要注意的地方。
先来看一段代码:
const s = new Set([1,2,3])
const p = new Proxy(s,{})
console.log(p.size)
结果报错
Uncaught TypeError: Method get Set.prototype.size called on incompatible receiver #<Set>
at get size (<anonymous>)
at <anonymous>:3:15
通过参阅规范得知,Set.prototype.size是一个访问器属性,在上例子中作为方法调用,并且size的set函数是undefined,其get函数会执行一下步骤:
关键点在第一步和第二步
首先第一步中Let S be the this value(设置S的值为this) 这里this指的是什么,由于是通过代理对象来访问size属性的,所以this就是代理对象p
然后第二步,Perform ? RequireInternalSlot(S, [[SetData]]) (调用抽象方法RequireInternalSlot(S, [[SetData]]) )来检查S是否存在内部槽[[SetData]]。显然代理对象S没有[[SetData]]这个内部槽,所以会抛出错误
为了修复这个问题,需要修改访问器的getter函数执行时的this指向,如下面代码所示
const s = new Set([1,2,3])
const p = new Proxy(s,{
get(target, key, receiver){
if(key==='size'){
// 如果读取的是size属性
// 通过指定第三个参数receiver为原始对象target从而修复问题
return Reflect.get(target, key, target)
}
// 读取其他属性则是默认行为
return Reflect.get(target, key, target)
}
})
接着尝试从Set中删除数据,如下面代码所示:
const s = new Set([1,2,3])
const p = new Proxy(s,{
get(target, key, receiver){
if(key === 'size'){
return Reflect.get(target, key, target)
}
return Reflect.get(target, key, receiver)
}
})
p.delete(1)
// 结果会报错
Uncaught TypeError: Method Set.prototype.delete called on incompatible receiver #<Set>
at Proxy.delete (<anonymous>)
at <anonymous>:11:9
这个错误和p.size报错的错误很相似,但是实际上,访问p.size与访问p.delete是不同的,size是一个属性而delete是一个方法,访问p.size时,getter函数会立即执行;而访问p.delete时,delete方法没有执行,真正执行的是p.delete(1)这句函数调用。因此无论怎么修改receiver,delete方法执行时的this都是指向代理对象p。要解决这个问题,就要把delete方法和原始数据对象绑定即可。代码如下:
const s = new Set([1,2,3])
const p = new Proxy(s,{
get(target, key, receiver){
if(key === 'size'){
return Reflect.get(target, key, target)
}
// 将方法雨原始数据对象target绑定后返回
return target[key].bind(target)
}
})
p.delete(1)
在上面代码中,使用target[key].bind(target)代替了Reflect.get(target,key,receiver)。使用bind函数将用于操作数据的方法和原始数据对象target做了绑定,这p.delete(1)执行时,delete函数的this总是指向数据对象而非代理对象。
最后将上面的代码封装到前面提到的createReactive函数中
const reactiveMap = new Map()
function reactive(obj){
const proxy = createReactive(obj)
const existionProxy = reactiveMap.get(obj)
reactiveMap.set(obj, proxy)
return proxy
}
// 在createReactive里封装用于代理Set/Map类型数据的逻辑
function createReactive(obj, isShallow = false, isReadonly = false){
return new Proxy(obj, {
get(target, key, receiver){
if(key === 'size'){
return Reflect.get(target, key, target)
}
// 将方法雨原始数据对象target绑定后返回
return target[key].bind(target)
}
}
})
}
这样就可以创建代理数据了
const p = reactive(new Set([1,2,3]))
console.log(p.size) // 3
知识扩展:
关于访问器属性
js有两种属性类型
数据属性:一般用于存储数据数值
访问器属性:一般进行get和set操作,不能直接存储数据数值
访问器属性
规范地址:https://tc39.es/ecma262/