首先来看下集合类型的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]’