【vue设计与实现】非原始值的响应式方案 10-如何代理Set和Map

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操作,不能直接存储数据数值

访问器属性

  • 不包括数据值
  • 包含set和get函数
  • 读取访问器属性,get函数返回有效值
  • 写入访问器属性,set函数处理数据
  • 不能直接定义,必须使用defineProperty定义

规范地址:https://tc39.es/ecma262/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值