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

首先来看下集合类型的forEach方法是如何工作的

const m = new Map([
	[
		{key:1},
		{value:1},
	]
])

effect(()=>{
	m.forEach(function(value, key, m){
		console.log(value) //{value:1}
		console.log(key) //{key:1}
	})
})

遍历操作只与键值对的数量有关,因此任何会修改Map对象键值对数量的操作都应该触发副作用函数重新执行,例如delete和add方法等。所以当forEach函数被调用时,应该让副作用函数与ITERATE_KEY建立响应联系,如下面代码所示:

const mutableInstrumenttations = {
	forEach(callback){
		// 取得原始数据对象
		const target = this.raw
		track(target, ITERATE_KEY)
		// 通过原始数据对象调用forEach方法,并把callback传递过去
		traget.forEach(callback)
	}
}

这样就实现了对forEach操作的追踪

然而,上面给出的forEach函数仍然存在缺陷,我们在自定义实现的forEach方法内,通过原始数据对象调用了原生的forEach方法,即 traget.forEach(callback)。这就以为着传递给callback的参数将是非响应式数据,这会导致下面的代码不会正常工作:

const key = {key:1}
const value = new Set([1,2,3])
const p = reactive(new Map([
	[key, value]
]))

effect(()=>{
	p.forEach(function(value, key){
		console.log(value.size) //3
	})
})

p.get(key).delete(1)

这里最后尝试删除操作时,没有触发副作用函数重新执行。就是因为,当通过value.size访问size属性时,这里的value是原始数据对象。
其实forEach方法的回调函数接收到的参数也应该是响应式数据才对。
为解决这个问题,需要对现有的实现进行修改:

const mutableInstrumenttations = {
	// 注意forEach可以接收第二个参数
	forEach(callback, thisArg){
		// wrap函数用来把可代理的值转换为响应式数据
		// 通过判断类型是否为object
		const wrap = (val) => typeof val === 'object'?reactive(val):val
		// 取得原始数据对象
		const target = this.raw
		track(target, ITERATE_KEY)
		// 通过原始数据对象调用forEach方法,并把callback传递过去
		traget.forEach((v,k)=>{
			// 手动调用callback,用wrap函数包裹value和key后再传给callback, 这样就实现了深响应
			// 通过.call调用callback, 并传递thisArg
			callback(thisArg, wrap(v),wrap(k),this)
		})
	}
}

这里要注意对forEach可以接收第二个参数的处理,这个参数可以用来指定callback函数执行时的this值

后续还有工作。无论是使用for…in循环遍历一个对象,还是使用forEach循环遍历一个集合,其响应联系都是建立在ITERATE_KEY与副作用函数之间的。但是for…in和forEach的不同点在于,使用for…in循环遍历对象时,它只关心对象的键,不关心对象的值
只有当新增,删除对象的key时,才需要重新执行副作用函数。而SET类型的操作,不会改变一个对象的键的数量,所以这类操作不需要触发副作用函数重新执行。
但是Map类型的forEach遍历,set操作也要触发响应,因为forEach遍历Map类型的数据时,即关心键也关心值
所以要对trigger进行如下修改:

function trigger(target, key, type, newVal){
	const depsMap = bucket.get(target)
	if(!depsMap) return
	const effects = depsMap.get(key)

	const effectsToRun = new Set()
	effects && effect.forEach(effectFn => {
		if(effectFn != activeEffect){
			effectsToRun.add(effectFn)
		}
	})

	if(
		type === 'ADD' ||
		type === 'DELETE' ||
		// 如果操作类型是SET并且目标兑现是Map类型的数据
		(
			type === 'SET' &&
			Object.prototype.toString.call(target) === '[object Map]'
		)
	){
		const iterateEffects = depsMap.get(ITERATE_KEY)
		iterateEffects && iterateEffects.forEach(effectFn => {
			if(effectFn != activeEffect){
				effectsToRun.add(effectFn)
			}
		})
	}

	// 省略部分内容
	effectsToRun.forEach(effectFn => {
		if(effectFn.options.scheduler){
			effectFn.options.scheduler(effectFn)
		}else{
			effectFn()
		}
	})
}

扩展
判断对象类型
Object.prototype.toString.call(target) === ‘[object Map]’

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值