非原始值的响应式方案

非原始值的响应式方案

参考非原始值的响应式方案
请结合上一篇文章阅读

理解Proxy和Reflect

Proxy

Proxy可以创建一个代理对象,能够实现对其他对象的代理。

代理:对一个对象的基本语义的代理,允许我们拦截并重新定义对一个对象的基本操作

基本语义:

  • 类似于读取属性值、设置属性值的操作,就属于基本语义的操作,即基本操作
  • 调用函数fn()也是对一个对象的基本操作

拦截函数操作

const sum = (left, right) => {
	return left + right
}
const p = new Proxy(sum, {
	apply(target, ctx, args) {
		return target.call(ctx, ...args) * 2
	},
})
console.log(p(1, 2)) // 6
console.log(p.call(null, 5, 6)) // 22
console.log(p.apply(null, [7, 8])) // 30

上面的代码中,每当执行proxy函数(直接调用或call和apply调用),都会被apply方法拦截

console.log(Reflect.apply(p, null, [9, 10]))

直接调用Reflect.apply方法,也会被拦截

复合操作:
Proxy只能拦截一个对象的基本操作,而不能拦截非基本操作

典型的非基本操作有:调用对象下面的方法obj.fn(),因为其是由两个基本语义组成的,第一个基本语义是get,即先通过get操作得到obj.fn属性,第二个基本语义是函数调用,即通过get得到obj.fn的值后再调用它,也就是上面的apply

Reflect

Reflect是一个全局对象,拥有许多方法:get、set、apply等

在Proxy拦截器中能找到的方法,在Reflect中能找到同名函数,但两者的使用有一点区别:
Reflect.get能接受第三个参数,即指定接受者receiver,也可以理解为函数调用过程中的this

const data = {
	foo: 1,
	get bar() {
		return this.foo
	},
}
console.log('Reflect.get', Reflect.get(data, 'bar', { foo: 555 })) // 555

上一节代码中存在的不足:

effect(() => {
	console.log(p.bar)
})
p.foo++

按理说p.foo++会执行副作用函数,但实际并没有,也就是副作用函数没有与foo之间建立响应式联系,因为使用p.bar访问属性bar时,getter函数内this实际指向原始对象data,最终访问的是data.foo

解决:
使用Reflect.get函数

const p = new Proxy(data, {
	get(target, key, receiver) {
		track(target, key)
		// return target[key]
		return Reflect.get(target, key, receiver)
	},
	set(target, key, newV) {
		// target[key] = newV
        Reflect.set(target, key, )
		trigger(target, key, newV, receiver)
		return true
	},
})

这时p.bar去访问 bar属性,第三个参数receiver就是p,getter函数中的this就是p。所以就与p.foo中的foo属性建立了联系。

代理Object

对一个普通对象的读取操作

  • 访问属性:obj.foo
  • 判断对象或原型上是否存在给定的key:key in obj
  • 使用foo...in循环遍历对象:for(const key in obj) {}
  • 删除属性:delete obj.foo

现在应该拦截所有的读取操作,以便当数据变化的时候能够正确触发响应

拦截属性读取操作

通过设置属性读取操作get拦截器

拦截in操作符

Proxy并没有in拦截器。我们可以通过has拦截函数实现对in操作符的代理

const p = new Proxy(data, {
	get(target, key, receiver) {
		track(target, key)
		return Reflect.get(target, key, receiver)
	},
	has(target, key, receiver) {
		track(target, key)
		return Reflect.get(target, key, receiver)
	}
})

拦截for...in循环

Proxy也没有for...in拦截器。我们可以通过ownkeys拦截函数来拦截

const ITERATE_KEY = Symbol()
const p = new Proxy(data, {
	get(target, key, receiver) {
		track(target, key)
		return Reflect.get(target, key, receiver)
	},
	has(target, key, receiver) {
		track(target, key)
		return Reflect.get(target, key, receiver)
	},
    ownKeys(target) {
		track(target, ITERATE_KEY)
		return Reflect.ownKeys(target)
	},
})

什么情况下对数据的操作需要触发与ITERATE_KEY相关联的副作用函数重新执行?

  • 副作用函数
effect(() => {
	for (const key in p) {
		console.log(key)
	}
})

  • 添加属性baz p.baz = 1
  • 添加属性之后应该让for...in循环重新执行,需要触发与ITERATE_KEY相关联的副作用函数重新执行,但此时并没有重新执行

问题出在set拦截函数上,set函数接受的key只是字符串baz,所以副作用函数也只是与baz建立的联系,而for...in是在副作用函数与ITERATE_KEY之间建立的联系,和baz没有关系,所以不能触发响应

  • 解决:添加属性时,将那些与ITERATE_KEY相关联的副作用函数也取出来执行
function trigger(target, key) {
	// 从桶中取出与target关联的depsMap
	let depsMap = bucket.get(target)
	if (!depsMap) return

	// 从depsMap中取出与key关联的Set(副作用函数集合)
	const effects = depsMap.get(key)
    // 从depsMap中取出与ITERATE_KEY关联的Set
	const iterateEffects = depsMap.get(ITERATE_KEY)

	// 用一个新的Set集合来遍历
	const effectsToRun = new Set(effects)
	effects &&
		effects.forEach(effectFn => {
			if (effectFn !== activeEffect) {
				effectsToRun.add(effectFn)
			}
		})
	iterateEffects &&
		iterateEffects.forEach(effectFn => {
			if (effectFn !== activeEffect) {
				effectsToRun.add(effectFn)
			}
		})
	effectsToRun.forEach(effectFn => {
		if (effectFn.options.scheduler) {
			effectFn.options.scheduler(effectFn)
		} else {
			effectFn()
		}
	})
}
  • 除了添加对象属性,还有修改对象属性
    p.baz = 2
  • 目前的情况是修改对象属性,会让for...in循环重新执行一遍

我们不希望其重新执行

  • 解决:在set拦截器中区分操作类型,并且将类型传递给trigger函数进行区分
const TriggerType = {
	SET: 'SET',
	ADD: 'ADD',
}
// 修改set拦截器
set(target, key, newV, receiver) {
    // 如果属性不存在,则说明添加新属性,否则就是设置已有属性
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
    // 修改属性值
    Reflect.set(target, key, newV, receiver)
    // 触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
    trigger(target, key, type)
    return true
}
// 修改trigger函数
function trigger(target, key, type) {
	// 从桶中取出与target关联的depsMap
	let depsMap = bucket.get(target)
	if (!depsMap) return

	// 从depsMap中取出与key关联的Set(副作用函数集合)
	const effects = depsMap.get(key)
	// 从depsMap中取出与ITERATE_KEY关联的Set
	const iterateEffects = depsMap.get(ITERATE_KEY)

	// 用一个新的Set集合来遍历
	const effectsToRun = new Set(effects)
	effects &&
		effects.forEach(effectFn => {
			if (effectFn !== activeEffect) {
				effectsToRun.add(effectFn)
			}
		})
	if (type === TriggerType.ADD) {
		iterateEffects &&
			iterateEffects.forEach(effectFn => {
				if (effectFn !== activeEffect) {
					effectsToRun.add(effectFn)
				}
			})
	}
	effectsToRun.forEach(effectFn => {
		if (effectFn.options.scheduler) {
			effectFn.options.scheduler(effectFn)
		} else {
			effectFn()
		}
	})
}

拦截delete操作符

Proxy也没有delete拦截器。我们可以通过deleteProperty拦截器来拦截

// 拦截delete操作
deleteProperty(target, key) {
    // 检查被操作的属性是否是对象自己的属性
    const hadKey = Object.prototype.hasOwnProperty.call(target, key)
    // 使用Reflect.deleteProperty完成属性的删除
    const res = Reflect.deleteProperty(target, key)
    // 只有当被删除的属性是对象自己的属性并且删除成功时,才触发更新
    if (res && hadKey) {
        trigger(target, key, 'DELETE')
    }
    return res
}

由于删除操作会让键变少,所以会影响for...in循环,所以应该触发与ITERATE_KEY相关联的副作用函数重新执行

// 修改trigger函数
function trigger(target, key, type) {
	// 从桶中取出与target关联的depsMap
	let depsMap = bucket.get(target)
	if (!depsMap) return

	// 从depsMap中取出与key关联的Set(副作用函数集合)
	const effects = depsMap.get(key)
	// 从depsMap中取出与ITERATE_KEY关联的Set
	const iterateEffects = depsMap.get(ITERATE_KEY)

	// 用一个新的Set集合来遍历
	const effectsToRun = new Set(effects)
	effects &&
		effects.forEach(effectFn => {
			if (effectFn !== activeEffect) {
				effectsToRun.add(effectFn)
			}
		})
	if (type === TriggerType.ADD || type === TriggerType.DELETE) {
		iterateEffects &&
			iterateEffects.forEach(effectFn => {
				if (effectFn !== activeEffect) {
					effectsToRun.add(effectFn)
				}
			})
	}
	effectsToRun.forEach(effectFn => {
		if (effectFn.options.scheduler) {
			effectFn.options.scheduler(effectFn)
		} else {
			effectFn()
		}
	})
}

合理触发响应

触发响应不仅需要以上工作,还有很多边界条件需要考虑

值没有发生变化

值没有发生变化的时候,不应该触发响应

effect(() => {
	console.log(p.foo)
})
// 设置p.foo的值,但是值没有变化
p.foo = 1

但是此时还是触发响应的,因为在set拦截中,只要绑定的值被设置,则需要重新触发副作用函数的执行

解决:修改set拦截函数,在调用trigger之前判断值是否真的改变了

特例:对NaN进行处理不能简单的判断新旧值是否全等 ,因为NaN === NaN会得到false

set(target, key, newV, receiver) {
    const oldVal = target[key]
    // 如果属性不存在,则说明添加新属性,否则就是设置已有属性
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
    // 修改属性值
    const res = Reflect.set(target, key, newV, receiver)
    // 比较新旧值
    if (oldVal !== newV && (oldVal === oldVal || newV === newV)) {
        // 触发副作用函数,并将type作为第三个参数传递给trigger函数
        trigger(target, key, type)
    }
    return true
},

从原型上继承属性

首先需要封装一个reactive函数,该函数接受一个对象作为参数,并返回其创建的响应式数据

function reactive(obj) {
	return new Proxy(obj, {
		// 省略各拦截函数
	})
}
  • 问题例子:

在下述代码中,最后一行让副作用函数重新执行了两次

const obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
Object.setPrototypeOf(child, parent)
effect(() => {
	console.log(child.bar)
})
child.bar = 2
  • 绑定分析:

在上述代码第7行中,要获取child.bar的值
所以必须调用child对象所部署的[[Get]]内部方法,所以会在child.bar与副作用函数之间建立响应联系

找不到child.bar的值之后,根据规范:就必须去parent对象上寻找,最终在parent上找到

由于parent也是响应式数据,所以也必须调用其部署的[[Get]]方法,此时parent.bar与副作用函数之间建立响应联系

  • 触发分析:

在上述代码的最后一行中,要设置child.bar

所以必须调用[[Set]]内部方法,由于与副作用函数建立了联系,所以此处就会触发一次副作用函数的执行

由于找不到要设置的属性,根据规范:则要去原型上面找,也就是parent,需要执行其[[Set]]内部方法,此时也会重新调用一次副作用函数。

  • 解决思路:

只需要屏蔽其中一次副作用函数的执行就可以了

我们选择屏蔽parent.bar触发的那次副作用函数的执行

  • 解决前置基础:

receiver参数在父子对象上的表现是有区别的
child对象:

set(target, key, newV, receiver) {
    // target是原始对象obj
    // receiver代理对象是child
}

parent对象:

set(target, key, newV, receiver) {
    // target是原始对象proto
    // receiver仍然代理对象是child
}

从这里可以看出,当parent代理对象的set拦截函数执行时,此时target是原始对象proto,而receiver仍然是代理对象child

receiver存在的意义就是为了正确的在陷阱中传递上下文,确保陷阱函数中调用者的正确的上下文访问
故proxy中接受的receiver形参表示代理对象本身或者继承于代理对象的对象

  • 解决:

由上述的前置知识,所有我们只需要判断receiver是否是target的代理对象即可,只有是target的代理对象才触发更新,这样就能屏蔽原型继承引起的更新了

首先需要给get拦截函数添加一个能力,让其访问raw能返回target

get(target, key, receiver) {
    // 代理对象可以通过raw属性访问原始数据
    if (key === 'raw') return target
    track(target, key)
    return Reflect.get(target, key, receiver)
}

然后在set拦截函数中判断receiver是不是target的代理对象即可

set(target, key, newV, receiver) {
    const oldVal = target[key]
    // 如果属性不存在,则说明添加新属性,否则就是设置已有属性
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
    // 修改属性值
    const res = Reflect.set(target, key, newV, receiver)
	// receiver是target的代理对象,才触发响应
    if (target === receiver.raw) {
        // 比较新旧值
        if (oldVal !== newV && (oldVal === oldVal || newV === newV)) {
            // 触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
            trigger(target, key, type)
        }
    }
    return true
}

浅响应和深响应

现在实现的receiver响应式都是浅响应,没有做到深响应

  • 问题:
    在下面代码中,在副作用函数中读取p.foo.bar,后续对其进行修改并不能让副作用函数重新执行
const p = reactive({ foo: { bar: 1 } })
effect(() => {
	console.log(p.foo.bar)
})
p.foo.bar = 2
  • 分析:

读取p.foo.bar的时候,首先要读取p.foo的值

我们使用Reflect.get函数返回p.foo的结果,由于通过该方法得到的结果是一个普通对象,并不是一个响应式数据,所以在副作用函数中访问p.foo.bar是不能建立响应联系的

所以修改该对象的时候不会触发副作用函数

  • 解决:

Reflect.get()的返回结果做一层封装

get(target, key, receiver) {
	// 代理对象可以通过raw属性访问原始数据
	if (key === 'raw') return target
	track(target, key)
	const res = Reflect.get(target, key, receiver)
	if (typeof res === 'object' && res !== null) {
		return reactive(res)
	}
	return res
},

这样就可以实现深响应了,但是并非所有情况都希望深响应,就催生了shallowReactive,即浅响应

也就是只有对象的第一层属性是响应式的

这样只需要对响应函数在进行一次封装即可:

function createReactive(obj, isShallow = false) {
	return new Proxy(obj, {
		get(target, key, receiver) {
			// 代理对象可以通过raw属性访问原始数据
			if (key === 'raw') return target
			track(target, key)
			const res = Reflect.get(target, key, receiver)
			if (isShallow) return res
			if (typeof res === 'object' && res !== null) {
				return reactive(res)
			}
			return res
		},

		has(target, key, receiver) {
			track(target, key)
			return Reflect.get(target, key, receiver)
		},
		ownKeys(target) {
			track(target, ITERATE_KEY)
			return Reflect.ownKeys(target)
		},
		set(target, key, newV, receiver) {
			const oldVal = target[key]
			// 如果属性不存在,则说明添加新属性,否则就是设置已有属性
			const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
			// 修改属性值
			const res = Reflect.set(target, key, newV, receiver)
			if (target === receiver.raw) {
				// 比较新旧值
				if (oldVal !== newV && (oldVal === oldVal || newV === newV)) {
					// 触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
					trigger(target, key, type)
				}
			}
			return true
		},
		// 拦截delete操作
		deleteProperty(target, key) {
			// 检查被操作的属性是否是对象自己的属性
			const hadKey = Object.prototype.hasOwnProperty.call(target, key)
			// 使用Reflect.deleteProperty完成属性的删除
			const res = Reflect.deleteProperty(target, key)
			// 只有当被删除的属性时对象自己的属性并且删除成功时,才触发更新
			if (res && hadKey) {
				trigger(target, key, 'DELETE')
			}
			return res
		},
	})
}

统一对外暴露接口:

// 深响应
function reactive(obj) {
	return createReactive(obj)
}
// 浅响应
function shallowReactive(obj) {
	return createReactive(obj, true)
}

只读和浅只读

只读

希望一些数据是只读的,而不可以做修改

如果用户要对其进行修改,就需要发出一条警告消息,这样就实现了对数据的保护

首先,为createReactive添加第三个参数isReadonly,并且修改setdeleteProperty函数,因为只读意味着既不可以设置属性值,也不可以删除

function createReactive(obj, isShallow = false, isReadonly = false) {
	return new Proxy(obj, {
		get(target, key, receiver) {
			// 代理对象可以通过raw属性访问原始数据
			if (key === 'raw') return target
			track(target, key)
			const res = Reflect.get(target, key, receiver)
			if (isShallow) return res
			if (typeof res === 'object' && res !== null) {
				return reactive(res)
			}
			return res
		},

		has(target, key, receiver) {
			track(target, key)
			return Reflect.get(target, key, receiver)
		},
		ownKeys(target) {
			track(target, ITERATE_KEY)
			return Reflect.ownKeys(target)
		},
		set(target, key, newV, receiver) {
			// 如果是只读的,则打印警告并返回
			if (isReadonly) {
				console.warn(`属性${key}是只读的`)
				return true
			}
			const oldVal = target[key]
			// 如果属性不存在,则说明添加新属性,否则就是设置已有属性
			const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
			// 修改属性值
			const res = Reflect.set(target, key, newV, receiver)
			if (target === receiver.raw) {
				// 比较新旧值
				if (oldVal !== newV && (oldVal === oldVal || newV === newV)) {
					// 触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
					trigger(target, key, type)
				}
			}
			return true
		},
		// 拦截delete操作
		deleteProperty(target, key) {
			// 如果是只读的,则打印警告并返回
			if (isReadonly) {
				console.warn(`属性${key}是只读的`)
				return true
			}
			// 检查被操作的属性是否是对象自己的属性
			const hadKey = Object.prototype.hasOwnProperty.call(target, key)
			// 使用Reflect.deleteProperty完成属性的删除
			const res = Reflect.deleteProperty(target, key)
			// 只有当被删除的属性时对象自己的属性并且删除成功时,才触发更新
			if (res && hadKey) {
				trigger(target, key, 'DELETE')
			}
			return res
		},
	})
}

其次,既然是只读属性,也就没有必要让其建立响应联系,所以在副作用函数读取一个只读属性的时候,不需要调用track函数进行追踪响应,所有我们应该修改get拦截函数

get(target, key, receiver) {
	// 代理对象可以通过raw属性访问原始数据
	if (key === 'raw') return target
	if (!isReadonly) {
		track(target, key)
	}
	const res = Reflect.get(target, key, receiver)
	if (isShallow) return res
	if (typeof res === 'object' && res !== null) {
		return reactive(res)
	}
	return res
},

深只读

在返回属性之前,判断是否是只读的,如果是只读的则再利用readonly进行包装,并把包装后的只读对象返回

get(target, key, receiver) {
	// 代理对象可以通过raw属性访问原始数据
	if (key === 'raw') return target
	if (!isReadonly) {
		track(target, key)
	}
	const res = Reflect.get(target, key, receiver)
	if (isShallow) return res
	if (typeof res === 'object' && res !== null) {
		return isReadonly ? readonly(res) : reactive(res)
	}
	return res
},

统一对外暴露接口

// 只读
function readonly(obj) {
	return createReactive(obj, false, true)
}
// 浅只读
function shallowReadonly(obj) {
	return createReactive(obj, true, true)
}

代理数组

数组是一个特殊的对象,上述大部分代码还可以正常使用

但是数组和对象还是有区别的:

  • 数组是一个异质对象,因为数组对象中的[[DefineOwnProperty]]与常规对象不同,但是其他内部方法的逻辑与常规对象相同
  • 数组的读取操作与普通对象存在不同,数组的读取操作:

通过索引访问元素
访问数组长度
把数组作为对象,使用for...in循环
使用for...of迭代遍历
数组的原型方法:concatjoineverysome

  • 数组的设置操作:

通过索引修改数组元素值
修改数组长度
数组的栈方法:pushpop
数组的原型方法:splicefill

数组索引与length

如果只是单纯的通过数组的索引值去设置元素的话,已经能够建立响应联系了

const arr = reactive(['foo'])
effect(() => {
	console.log(arr[0])
})
arr[0] = 'bar'

但是通过索引值设置数组元素与设置对象属性值还存在根本上的不同,因为数组对象部署的内部方法[[DefineOwnProperty]]不同于常规对象:

规范中对其的描述:
如果设置的索引值大于当前数组的长度,那么就要更新数组的长度
所以通过索引设置元素值得时候,可能会隐式修改length的属性值,触发响应时,也应该触发于length属性相关联的副作用函数重新执行

  • 问题一:修改索引值引起length变化
const arr = reactive(['foo'])
effect(() => {
	console.log(arr.length)
})
arr[1] = 'bar' // 应该能够重新触发副作用函数的执行
  • 实现:
    首先需要修改set拦截函数,让其对数组做一个判断,并区分SETADD操作
set(target, key, newV, receiver) {
	// 如果是只读的,则打印警告并返回
	if (isReadonly) {
		console.warn(`属性${key}是只读的`)
		return true
	}
	const oldVal = target[key]
	// 如果属性不存在,则说明添加新属性,否则就是设置已有属性
	const type = Array.isArray(target)
		? // 如果代理目标是数组,则检测设置的索引值是否小于数组长度
			// 如果是,则视为SET操作,否则视为ADD操作
			Number(key) < target.length
			? 'SET'
			: 'ADD'
		: Object.prototype.hasOwnProperty.call(target, key)
		? 'SET'
		: 'ADD'
	// 修改属性值
	const res = Reflect.set(target, key, newV, receiver)
	if (target === receiver.raw) {
		// 比较新旧值
		if (oldVal !== newV && (oldVal === oldVal || newV === newV)) {
			// 触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
			trigger(target, key, type)
		}
	}
	return true
},

其次,要在trigger函数中针对不同的操作进行修改

function trigger(target, key, type) {
	// 从桶中取出与target关联的depsMap
	let depsMap = bucket.get(target)
	if (!depsMap) return

	// 从depsMap中取出与key关联的Set(副作用函数集合)
	const effects = depsMap.get(key)
	// 从depsMap中取出与ITERATE_KEY关联的Set
	const iterateEffects = depsMap.get(ITERATE_KEY)

	// 用一个新的Set集合来遍历
	const effectsToRun = new Set(effects)
	effects &&
		effects.forEach(effectFn => {
			if (effectFn !== activeEffect) {
				effectsToRun.add(effectFn)
			}
		})
	if (type === TriggerType.ADD || type === TriggerType.DELETE) {
		iterateEffects &&
			iterateEffects.forEach(effectFn => {
				if (effectFn !== activeEffect) {
					effectsToRun.add(effectFn)
				}
			})
	}
	// 当操作类型为ADD且目标对象是数组的时候,应该取出并执行那些与length属性相关联的副作用函数
	if (type === TriggerType.ADD && Array.isArray(target)) {
		// 取出与length想关联的副作用函数
		const lengthEffects = depsMap.get('length')
		// 将这些副作用函数添加到effectsToRun中执行
		lengthEffects &&
			lengthEffects.forEach(effectFn => {
				if (effectFn !== activeEffect) {
					effectsToRun.add(effectFn)
				}
			})
	}
	effectsToRun.forEach(effectFn => {
		if (effectFn.options.scheduler) {
			effectFn.options.scheduler(effectFn)
		} else {
			effectFn()
		}
	})
}
  • 问题二:数组的length属性影响数组元素
const arr = reactive(['foo'])
effect(() => {
	console.log(arr[0])
})
arr.length = 0
  • 实现:

由于只有当索引值大于或等于新的length属性才需要重新触发响应,所以我们在调用trigger函数的时候,把新的属性值也传递过去

set(target, key, newV, receiver) {
	// 如果是只读的,则打印警告并返回
	if (isReadonly) {
		console.warn(`属性${key}是只读的`)
		return true
	}
	const oldVal = target[key]
	// 如果属性不存在,则说明添加新属性,否则就是设置已有属性
	const type = Array.isArray(target)
		? // 如果代理目标是数组,则检测设置的索引值是否小于数组长度
			// 如果是,则视为SET操作,否则视为ADD操作
			Number(key) < target.length
			? 'SET'
			: 'ADD'
		: Object.prototype.hasOwnProperty.call(target, key)
		? 'SET'
		: 'ADD'
	// 修改属性值
	const res = Reflect.set(target, key, newV, receiver)
	if (target === receiver.raw) {
		// 比较新旧值
		if (oldVal !== newV && (oldVal === oldVal || newV === newV)) {
			// 触发函数,触发副作用函数,并将type作为第三个参数传递给trigger函数
			trigger(target, key, type, newV)
		}
	}
	return true
},

修改trigger函数

function trigger(target, key, type, newV) {
	// 从桶中取出与target关联的depsMap
	let depsMap = bucket.get(target)
	if (!depsMap) return

	// 从depsMap中取出与key关联的Set(副作用函数集合)
	const effects = depsMap.get(key)
	// 从depsMap中取出与ITERATE_KEY关联的Set
	const iterateEffects = depsMap.get(ITERATE_KEY)

	// 用一个新的Set集合来遍历
	const effectsToRun = new Set(effects)
	effects &&
		effects.forEach(effectFn => {
			if (effectFn !== activeEffect) {
				effectsToRun.add(effectFn)
			}
		})
	if (type === TriggerType.ADD || type === TriggerType.DELETE) {
		iterateEffects &&
			iterateEffects.forEach(effectFn => {
				if (effectFn !== activeEffect) {
					effectsToRun.add(effectFn)
				}
			})
	}
	// 当操作类型为ADD切目标对象是数组的时候,应该取出并执行那些与length属性相关联的副作用函数
	if (type === TriggerType.ADD && Array.isArray(target)) {
		// 取出与length想关联的副作用函数
		const lengthEffects = depsMap.get('length')
		// 将这些副作用函数添加到effectsToRun中执行
		lengthEffects &&
			lengthEffects.forEach(effectFn => {
				if (effectFn !== activeEffect) {
					effectsToRun.add(effectFn)
				}
			})
	}
	// 如果操作目标是数组,并且修改了数组的length属性
	if (Array.isArray(target) && key === 'length') {
		// 对于所有大于或等于新的length值的元素
		// 需要把所有相关联的副作用函数取出并添加到effectToRun中
		depsMap.forEach((effects, key) => {
			if (key >= newV) {
				effects.forEach(effectFn => {
					if (effectFn !== activeEffect) {
						effectsToRun.add(effectFn)
					}
				})
			}
		})
	}
	effectsToRun.forEach(effectFn => {
		if (effectFn.options.scheduler) {
			effectFn.options.scheduler(effectFn)
		} else {
			effectFn()
		}
	})
}

遍历数组

使用for...in遍历数组:

  • 本质上数组也是一个对象,而数组对象和常规对象的不同仅体现在[[DefineOwnProperty]]这个内部方法上,所以使用for...in循环遍历数组与常规对象并没有差异,因此也是同样在ownKeys拦截函数上进行拦截
  • 在普通对象中,只有添加或删除属性才会影响for...in循环的结果;但是数组不同,数组本质上只要length属性被修改,那么for..in循环对数组的遍历结果就会改变,所以这时候该重新触发响应
  • 实现:在ownKeys中判断当前操作目标是不是数组,是的话则使用length作为key去建立响应联系
ownKeys(target) {
	track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
	return Reflect.ownKeys(target)
},

使用for...of遍历数组

  • for...of遍历是用来遍历可迭代对象的,可迭代对象就是该对象或该对象的原型实现了@@iterator方法,而@@iterator也就是Symbol.iterator这个值
  • for...of的规范中,可以看到数组迭代器的执行会读取数组的length属性,如果迭代的是数组元素值,那么还会读取数组的索引
// 模拟实现代码
const arr = [1, 2, 3, 4, 5]
arr[Symbol.iterator] = function () {
	const target = this
	const len = target.length
	let index = 0
	return {
		next() {
			return {
				value: index < len ? target[index] : undefined,
				done: index++ >= len,
			}
		},
	}
}
  • 从上述代码可以看出,只需要在副作用函数与数组长度和索引直接建立响应式联系即可

而我们之前的代码中,已经实现了该需求,所有我们无需改动代码就能实现响应for...of

数组的values方法:

  • 数组values方法的返回值实际上就是数组内建的迭代器
console.log(Array.prototype.values === Array.prototype[Symbol.iterator]) // true
  • 所以不增加任何代码的情况下,也能让数组的迭代器方法正常工作

for...ofvalues存在的问题:
两者在直接修改length的时候都会报错,因为不管是调用for...of还是调用values,都会读取数组上的Symbol.iterator属性,该属性是一个Symbol值,所以为了避免发生上诉错误,以及性能消耗,我们不应该在副作用函数与这类symbol之间建立响应联系,所以应该修改get拦截函数:

get(target, key, receiver) {
	// 代理对象可以通过raw属性访问原始数据
	if (key === 'raw') return target
	// 非只读并且key不为symbol的时候才建立响应联系
	if (!isReadonly && typeof key !== 'symbol') {
		track(target, key)
	}
	const res = Reflect.get(target, key, receiver)
	if (isShallow) return res
	if (typeof res === 'object' && res !== null) {
		return isReadonly ? readonly(res) : reactive(res)
	}
	return res
},

数组的查找方法

通过上文,我们已经知道数组方法内部其实都依赖了对象的基本语义,所以大多数情况下,我们并不需要做处理就可以让这些方法按照预期工作

例如includes

正常情况下通过includes查找给定元素:

const arr = reactive(['foo', 'bar'])
effect(() => {
	console.log(arr.includes('foo'))
})
arr[0] = 1

但是,includes方法并不会总是按照预期执行:

const obj = { name: '111' }
const obj1 = { name: '222' }
const arr = reactive([obj, obj1])
console.log(arr.includes(arr[1])) // false
  • arr[1]通过代理对象arr访问元素的值,会得到一个新的代理对象。
  • includes方法内部会通过索引读取元素的值,也会得到一个新的代理对象。
  • 两个代理对象并不相同,所以输出false

解决:

定义一个map结构存储原始对象到代理对象的映射,每次调用reactive之前先来这里查找有没有对应的代理对象,有的话直接返回,没有的话则创建,并将其新创建的代理对象存储到这个结构中

// 定义一个Map实例,存储原始对象到代理对象的映射
const reactiveMap = new Map()
// 深响应
function reactive(obj) {
	const existionProxy = reactiveMap.get(obj)
	if (existionProxy) return existionProxy
	const proxy = createReactive(obj)
	reactiveMap.set(obj, proxy)
	return proxy
}

新问题:

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj));	//false

arr.includes获取的数组元素得到的值是代理对象,obj是原始对象,因此返回false

解决:
重写includes方法,由于includes方法本质上也就是读取代理对象arr的includes属性,所以也会触发get拦截函数,所以我们让其返回自定义的includes方法

get(target, key, receiver) {
	// 代理对象可以通过raw属性访问原始数据
	if (key === 'raw') return target
	// 如果操作的目标对象是数组,并且key存在于arrayInstrumentations上
	// 那么返回定义在arrayInstrumentations上的值,返回的值也就是重新定义的includes
	if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
		return Reflect.get(arrayInstrumentations, key, receiver)
	}
	// 非只读并且key不为symbol的时候才建立响应联系
	if (!isReadonly && typeof key !== 'symbol') {
		track(target, key)
	}
	const res = Reflect.get(target, key, receiver)
	if (isShallow) return res
	if (typeof res === 'object' && res !== null) {
		return isReadonly ? readonly(res) : reactive(res)
	}
	return res
},

自定义includes方法,其中第五行是先实现includes的默认行为,如果找不到再去原始数组中查找

const originMethod = Array.prototype.includes
const arrayInstrumentations = {
	includes: function (...args) {
		// this是代理对象,先在代理对象中查找,将结果存储到res中
		let res = originMethod.apply(this, args)
		// 没有找到
		if (res === false) {
			// 通过this.raw拿到原始数据,在原始数据中查找并更新res
			res = originMethod.apply(this.raw, args)
		}
		return res
	},
}

类似数组方法indexOf、lastIndexOf也要进行重写:

const arrayInstrumentations = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
	const originMethod = Array.prototype[method]
	arrayInstrumentations[method] = function (...args) {
		// this是代理对象,先在代理对象中查找,将结果存储到res中
		let res = originMethod.apply(this, args)
		// 没有找到
		if (res === false || res === -1) {
			// 通过this.raw拿到原始数据,在原始数据中查找并更新res
			res = originMethod.apply(this.raw, args)
		}
		return res
	}
})

隐式修改数组长度的原型方法

这种隐式修改数组长度的方法主要是数组的栈方法:push、pop、shift、unshift

通过push的规范,我们知道,该流程中不仅需要读取数组的length属性,还要设置这个属性。所以这样的话,会导致两个独立的副作用函数相互影响,最终导致爆栈

const arr = reactive([])
effect(() => {
	arr.push(1)
})
effect(() => {
	arr.push(2)
})

分析:

  1. 第一个副作用函数执行,会读取length属性,所以第一个副作用函数执行完毕后,会与length建立响应联系
  2. 第二个副作用函数执行的时候,因为会读取length属性,所以会与length建立响应联系
  3. 现在第二个副作用函数由于push会间接设置length的值,导致会把length关联的所有副作用函数全部取出执行,即第一个副作用会被取出执行
  4. 第二个副作用函数还没有执行完毕,第一个副作用函数就会开始执行,第一个副作用函数开始执行会导致第二个副作用函数执行,循环往复则导致调用栈溢出。

解决:
问题的核心就是push方法调用会间接读取length属性,所以只要屏蔽对length属性的读取,就能避免与副作用函数建立响应联系,故需要重写push方法:

// 标记变量,代表是否进行追踪,默认值允许追踪
let shouldTrack = true
// 重写数组push方法
;['push'].forEach(method => {
	const originMethod = Array.prototype[method]
	arrayInstrumentations[method] = function (...args) {
		// 调用原始方法之前停止追踪
		shouldTrack = false
		let res = originMethod.apply(this, args)
		// 调用原始方法之后,恢复原来的行为
		shouldTrack = track
		return res
	}
})

这样在调用默认方法之前先禁止追踪,调用完在恢复原来行为,所以还要改写一下track函数:

function track(target, key) {
	// 没有activeEffect,则return
	if (!activeEffect || !shouldTrack) return
	// ...
}

代理Set和Map

如何代理Set和Map

由于SetMap结构是通过特定的属性和方法来操作自身的,与普通对象并不一样,所以不能用普通对象的响应式操作

size方法调用报错

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>

根据Set.prototype.size规范,代理对象Proxy对象不存在内部槽[[SetData]],所以就会抛出异常

解决:

get(target, key, receiver) {
	// 拦截读取size属性的行为
	if (key === 'size') {
		// 通过指定第三个参数receiver为原始对象target来解决问题
		// 因为原始对象存在[[SetData]]内部槽
		return Reflect.get(target, key, target)
	}
	// ...省略
}

delete方法调用报错

p.delete(1)

分析:

  1. 此处的报错与size十分相似,但是两者的访问又是不同的

  2. 当访问p.size的时候,访问器属性的getter函数会立即执行,所以我们可以通过修改receiver来改变getter函数的this执行

  3. 而访问delete的时候,delete方法并没有执行,真正使其执行的语句是p.delete(1)这句函数调用

  4. 所以无论我们怎么修改receiverdelete方法执行时this都会指向代理对象p,而不会指向原始对象。

解决:
delete方法与原始数据对象绑定即可

get(target, key, receiver) {
	//  ...省略
	if (key === 'delete') {
		return target[key].bind(target)
	}
	return res
}

建立响应联系

  • 目标:
    建立响应联系之后,使用add能够触发响应
  • 实现:
    进行依赖追踪:
    此处需要使用ITERATE_KEY与副作用函数之间建立联系,因为任何新增、删除操作都会影响size属性
get(target, key, receiver) {
	// 拦截读取size属性的行为
	if (key === 'size') {
		track(target, ITERATE_KEY)
		// 通过指定第三个参数receiver为原始对象target来解决问题
		// 因为原始对象存在[[SetData]]内部槽
		return Reflect.get(target, key, target)
	}
	// 代理对象可以通过raw属性访问原始数据
	if (key === 'raw') return target
	return mutableInstrumentations[key]
}

重写add方法:
此处定义一个对象mutableInstrumentations,存储所有自定义方法

// 将所有自定义实现的方法定义到该对象上
const mutableInstrumentations = {
	// 自定义add方法
	add(key) {
		const target = this.raw
		const res = target.add(key)
		trigger(target, key, 'ADD')
		return res
	},
}

优化:
只有添加的元素不存在原来的Set集合中才触发响应,否则不触发

// 自定义add方法
add(key) {
	const target = this.raw
	// 判断值是否已经存在
	const hadKey = target.has(key)
	const res = target.add(key)
	// 只有值不存在的情况下才触发响应
	if (!hadKey) trigger(target, key, 'ADD')
	return res
},
  • 按照同样的思路,实现delete
// 自定义delete方法
delete(key) {
	const target = this.raw
	// 判断值是否已经存在
	const hadKey = target.has(key)
	// 删除值
	const res = target.delete(key)
	// 只有值存在的情况下才触发响应
	if (hadKey) trigger(target, key, 'DELETE')
	return res
},

避免污染原始数据

首先,由于要借助map类型的数据开讲解这一节,所以我们先实现一下getset方法

// 自定义get方法
get(key) {
	const target = this.raw
	const hadKey = target.has(key)
	track(target, key)
	if (hadKey) {
		const res = target.get(key)
		// 如果得到的结果res仍然是可代理的数据,则要返回reactive包装后的响应式数据
		return typeof res === 'object' ? reactive(res) : res
	}
},
// 自定义set方法
set(key, value) {
	const target = this.raw
	const hadKey = target.has(key)
	const oldVal = target.get(key)
	target.set(key, value)
	if (!hadKey) {
		// 如果不存在,则说明是新增,所以是ADD操作类型
		trigger(target, key, 'ADD')
	} else if (oldVal !== value || (oldVal === oldVal && value === value)) {
		// 如果存在,并且值变了
		trigger(target, key, 'SET')
	}
},
const m = new Map()
const p1 = reactive(m)
const p2 = reactive(new Map())
// p1有一个键值对是代理对象p2
p1.set('p2', p2)
effect(() => {
	//通过原始数据m访问p2
	console.log(m.get('p2').size)
})
//通过原始数据m为p2设置一个键值对foo ---> 1
m.get('p2').set('foo', 1) // 这里会触发副作用函数重新执行

分析:
p1.set('p2', p2) set方法之中,我们直接把响应式数据p2设置到原始对象target.set(key, value)上了,这种响应式数据设置到原始数据上的行为称为数据污染。

解决:
只需要在调用target.set之前对值进行检查即可,如果是响应式数据,则通过raw属性获取原始数据,再把原始数据设置到target

// 自定义set方法
set(key, value) {
	const target = this.raw
	const hadKey = target.has(key)
	const oldVal = target.get(key)
	// 获取原始数据,由于value本身可能是原始数据了,此时value.raw并不存在,则直接使用value
	const rawValue = value.raw || value
	target.set(key, rawValue)
	if (!hadKey) {
		// 如果不存在,则说明是新增,所以是ADD操作类型
		trigger(target, key, 'ADD')
	} else if (oldVal !== value || (oldVal === oldVal && value === value)) {
		// 如果存在,并且值变了
		trigger(target, key, 'SET')
	}
},

处理forEach

由于遍历操作与键值对的数量有关,所以任何会修改Map对象键值对数量的操作都应该触发副作用函数的重新执行,例如adddelete等方法

所以forEach函数被调用的时候,我们都应该让副作用函数与ITERATE_KEY建立响应联系:

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

但上述代码还不完善,现在有一个问题:

const key = { key: 1 }
const value = new Set([1, 2, 3])
const p = reactive(new Map([[key, value]]))
effect(() => {
	p.forEach((value, key) => {
		console.log(value.size)
	})
})
p.get(key).delete(1)

在上述代码中,执行了最后一行代码后,并未能重新触发副作用函数,这是不符合逻辑的,因为我们通过reactive去代理对象,也就是想要使用深响应,所以代理对象的值如果发生变化的话,应该触发副作用函数重新执行

原因就是使用value.size去访问size的时候,由于value是原始数据类型,所以并不会建立响应联系,要解决这个问题,只要将callback的参数转化成响应式的即可

// 自定义forEach方法
forEach(callback) {
	// warp函数可以把代理的值转化成响应式数据
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	const target = this.raw
	track(target, ITERATE_KEY)
	//通过原始数据对象调用forEach方法,并把callback传递过去
	target.forEach((v, k) => {
		// 手动调用callback,用wrap函数包裹value和key后再传给callback,实现深响应
		callback(wrap(v), wrap(k), this)
	})
},

完善上述forEach代码,由于forEach还可以接收第二个参数thisArg,可以指定callback函数执行时this值:

// 自定义forEach方法
forEach(callback, thisArg) {
	// warp函数可以把代理的值转化成响应式数据
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	const target = this.raw
	track(target, ITERATE_KEY)
	//通过原始数据对象调用forEach方法,并把callback传递过去
	target.forEach((v, k) => {
		// 手动调用callback,用wrap函数包裹value和key后再传给callback,实现深响应
		// 通过call调用callback,并传递thisArg
		callback.call(thisArg, wrap(v), wrap(k), this)
	})
},

现在for...inforEach遍历对象都是建立在ITERATE_KEY与副作用函数之间,但是两者还是存在本质的不同:for...in循环遍历对象,只关心键,不关心值,只有新增删除操作才能触发副作用函数重新执行,而这个规则不适用于forEach

所以现在修改Map数据的时候,forEach不会被触发执行,故应该修改trigger

function trigger(target, key, type, newV) {
	// 省略...
	if (
		type === TriggerType.ADD ||
		type === TriggerType.DELETE ||
		// 如果操作类型是SET,并且目标对象是Map类型的数据,那么也应该触发于ITERATE_KEY关联的副作用函数
		(type === TriggerType.SET && Object.prototype.toString.call(target) === '[object Map]')
	) {
		iterateEffects &&
			iterateEffects.forEach(effectFn => {
				if (effectFn !== activeEffect) {
					effectsToRun.add(effectFn)
				}
			})
	}
	// 省略...
}

迭代器方法

集合类型有三个迭代器方法:entries、keys、values
调用上面的方法会返回迭代器对象,可以使用for...of循环遍历
其中,entries[[Symbol.iterator]]是等价的,两者都可以使用for...of进行迭代

const m = new Map([
	['key1', 'value1'],
	['key2', 'value2'],
])
for (const [key, value] of m.entries()) {
	console.log(key, value)
}
for (const [key, value] of m) {
	console.log(key, value)
}

直接使用[[Symbol.iterator]]也可以获得迭代器对象,可以手动调用迭代器的next方法获取相应的值

初步实现响应式:

const m = new Map([
	['key1', 'value1'],
	['key2', 'value2'],
])
const p = reactive(m)
effect(() => {
	for (const [key, value] of p) {
		console.log(key, value)
	}
})
p.set('key3', 'value3')
//TypeError: p is not iterable

执行上面这段代码会报错,因为p并不是一个迭代器,而是一个代理对象

所以现在我们将[[Symbol.iterator]]添加到mutableInstrumentations

// 自定义[[Symbol.iterator]]方法
[Symbol.iterator]() {
	const target = this.raw
	// 获取原始迭代器方法
	const itr = target[Symbol.iterator]()
	return itr
},

接下来要一步步完善这个自定义方法

首先,与forEach一样,如果迭代产生的值也是可以被代理的,那么应该将其包装成响应式数据

// 自定义[[Symbol.iterator]]方法
[Symbol.iterator]() {
	const target = this.raw
	// 获取原始迭代器方法
	const itr = target[Symbol.iterator]()
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	// 返回自定义迭代器对象
	return {
		next() {
			const { value, done } = itr.next()
			return {
				// 如果value不是undefined,则对其进行包装
				value: value ? [wrap(value[0]), wrap(value[1])] : value,
				done,
			}
		},
	}
},

然后,要对for...of进行追踪,调用track函数,让副作用函数与ITERATE_KEY建立联系

由于迭代操作与集合中元素数量有关,所有只有集合的size发生变化,就应该触发副作用函数的重新执行

// 自定义[[Symbol.iterator]]方法
[Symbol.iterator]() {
	const target = this.raw
	// 获取原始迭代器方法
	const itr = target[Symbol.iterator]()
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	// 与ITERATE_KEY建立响应式
	track(target, ITERATE_KEY)
	// 返回自定义迭代器对象
	return {
		next() {
			const { value, done } = itr.next()
			return {
				// 如果value不是undefined,则对其进行包装
				value: value ? [wrap(value[0]), wrap(value[1])] : value,
				done,
			}
		},
	}
},

其次,由于p.entriesp[Symbol.iterator]等价,所以可以使用同样的方式对p.entries进行拦截

const mutableInstrumentations = {
    //共用一个方法
    [Symbol.iterator]: iterationMethod,
    entries: iterationMethod
}
function iterationMethod() {
	const target = this.raw
	// 获取原始迭代器方法
	const itr = target[Symbol.iterator]()
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	// 与ITERATE_KEY建立响应式
	track(target, ITERATE_KEY)
	// 返回自定义迭代器对象
	return {
		next() {
			const { value, done } = itr.next()
			return {
				// 如果value不是undefined,则对其进行包装
				value: value ? [wrap(value[0]), wrap(value[1])] : value,
				done,
			}
		},
	}
}

最后

for (const [key, value] of p[Symbol.iterator]()) {
	console.log(key, value)
}
//p[Symbol.iterator] is not a function or its return value is not iterable

报错,是因为返回值不是一个可迭代对象。

所以我们要为其增加一个Symbol.iterator方法,让返回的对象同时实现可迭代协议和迭代器协议

  • 可迭代协议:一个对象实现了Symbol.iterator方法
  • 迭代器协议:一个对象实现了next方法
function iterationMethod() {
	const target = this.raw
	// 获取原始迭代器方法
	const itr = target[Symbol.iterator]()
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	track(target, ITERATE_KEY)
	// 返回自定义迭代器对象
	return {
		// 实现迭代器协议
		next() {
			const { value, done } = itr.next()
			return {
				// 如果value不是undefined,则对其进行包装
				value: value ? [wrap(value[0]), wrap(value[1])] : value,
				done,
			}
		},
		// 实现可迭代协议
		[Symbol.iterator]() {
			return this
		},
	}
}

values与keys方法

values方法返回的仅仅是数据的值,所以只需要对iterationMethod方法修改一下即可

const mutableInstrumentations = {
    //自定义values方法
    values: valuesIterationMethod,
}
function valuesIterationMethod() {
	const target = this.raw
	// 获取原始迭代器方法
	const itr = target.values()
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	track(target, ITERATE_KEY)
	// 返回自定义迭代器对象
	return {
		// 实现迭代器协议
		next() {
			const { value, done } = itr.next()
			return {
				// value是值,而非键值对,所以只需要包装value即可
				value: wrap(value),
				done,
			}
		},
		// 实现可迭代协议
		[Symbol.iterator]() {
			return this
		},
	}
}

实现keysvalues基本一样

function keysIterationMethod() {
	const target = this.raw
	// 获取原始迭代器方法
	const itr = target.keys()
	const wrap = val => (typeof val === 'object' ? reactive(val) : val)
	track(target, ITERATE_KEY)
	// 返回自定义迭代器对象
	return {
		// 实现迭代器协议
		next() {
			const { value, done } = itr.next()
			return {
				// value是值,而非键值对,所以只需要包装value即可
				value: wrap(value),
				done,
			}
		},
		// 实现可迭代协议
		[Symbol.iterator]() {
			return this
		},
	}
}

但是上述代码存在一个问题,如果尝试设置一个值,就会出乎意料的重新调用副作用函数

const m = new Map([
	['key1', 'value1'],
	['key2', 'value2'],
])
const p = reactive(m)
effect(() => {
	for (const value of p.keys()) {
		console.log(value)
	}
})
// 因为keys并不关心值,所以修改值不应该触发副作用函数重新执行
p.set('key2', 'value3')

原因:之前对Map类型的数据进行了特殊处理,操作类型为SET的时候,会触发与ITERATE_KEY相关联的副作用函数,而这种处理方式对于valuesentries是必须的,但是对于keys并没有必要

解决:只要重新定义一个MAP_KEY_ITERATE_KEY新的symbol值:

const MAP_KEY_ITERATE_KEY = Symbol()
function trigger(target, key, type, newVal){
    /** 省略部分代码 */
    //只有操作类型为ADD或DELETE并且为map的数据结构
    if ((type === TriggerType.ADD || type === TriggerType.DELETE ) && 
        //如果操作类型是SET,并且目标对象是Map类型的数据,那么也应该触发于ITERATOR_KEY关联的副作用函数
        Object.prototype.toString.call(target) === '[object Map]'
    ) {
        //取出那些与MAP_KEY_ITERATE_KEY相关联的副作用函数并执行
        const iterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY)
        iterateEffects && iterateEffects.forEach(effectFn => {
            if (effectFn !== activeEffect) effectsToRun.add(effectFn)
        })
    }
    /** 省略部分代码 */
}
//keys自定义方法的实现逻辑
function keysIterationMethod(){
    const target = this.raw
    const itr = target.keys()
    const wrap = (val) => typeof val === 'object' ? reactive(val) : val
    //调用track函数建立响应联系,要用MAP_KEY_ITERATE_KEY建立响应联系
    track(target, MAP_KEY_ITERATE_KEY)
    return  {
        next(){
            const {value, done} = itr.next()
            return {
                value: wrap(value),
                done
            }
        },
        [Symbol.iterator](){
            return this
        }
    }
}

原文链接:https://juejin.cn/post/7255561137583636536 作者:小野_

(0)
上一篇 2023年7月15日 上午10:05
下一篇 2023年7月15日 上午10:15

相关推荐

发表回复

登录后才能评论