【vue设计与实现】非原始值的响应式方案 4-合理地触发响应

【vue设计与实现】非原始值的响应式方案 4-合理地触发响应,第1张

首先当值没有发生变化时,应该不需要触发响应,看下面代码:

const obj = {foo:1}
const p = new Proxy(obj, {/* ... */})
effect(()=>{
	console.log(p.foo)
})

// 设置p.foo的值,但值没有变化
p.foo = 1

如上面的代码,如果值没有发生变化,则不需要触发响应。那么就要在set拦截函数中,在调用trigger函数触发响应前,需要检查值是否真的发生了变化

const p = new Proxy(obj, {
	set(target, key, newVal, receiver){
		// 先获取旧值
		const oldVal = target[key]
		const type = Object.propotype.hasOwnProperty.call(target, call)?''SET':'ADD'
		const res = Reflect.set(target, key, newVal, receiver)
		// 比较新值与旧值,只要当不全等的时候才触发响应
		if(oldVal !== newVal){
			trigger(target, key, type)
		}
		return res
	}
})

但是这样比较有问题,NaN !== NaN是总是返回true
也就是说,如果p.foo的初始值是NaN,而后续又为其设置了NaN为新值,那么仅仅进行全等比较就是有问题的。
因此要再加一个条件,如下:

const p = new Proxy(obj, {
	set(target, key, newVal, receiver){
		// 先获取旧值
		const oldVal = target[key]
		const type = Object.propotype.hasOwnProperty.call(target, call)?''SET':'ADD'
		const res = Reflect.set(target, key, newVal, receiver)
		// 比较新值与旧值,只要当不全等的时候,并且都不是NaN的时候才触发响应
		if(oldVal !== newVal && (oldVal === oldVal || newVal === newVal)){
			trigger(target, key, type)
		}
		return res
	}
})

但是仅仅处理关于NaN还不够,接下来,我们讨论一种从原型上继承属性的情况。不过首先我们需要封装一个reactive函数,该函数接收一个对象作为参数,并返回为其创建的响应式数据

function reactive(obj){
	return new Proxy(obj, {
		// 省略前文讲解的拦截函数
	})
}

可以看到,reactive函数只是对Proxy进行了一层封装。接下来,基于reactive创建一个例子:

const obj = {}
const proto = {bar:1}
const child = reactive(obj)
const parent = reactive(proto)
// 使用parent作为child的原型
Object.setPrototypeOf(child, parent)

effect(()=>{
	console.log(child.bar) // 1
})
// 修改child.bar的值
child.bar = 2 // 会导致副作用函数重新执行两次

注意这里child本身并没有bar属性,因此当访问child.bar时,值是从原型上继承来的。不过child是响应式数据,它与副作用函数之间会建立联系,因此当执行child.bar = 2时,副作用函数会重新执行,但是副作用函数执行了两次。

为了搞清楚问题的原因,我们要逐步分析整个过程:
当在副作用函数中读取child.bar的值时,会触发child代理对象的get拦截函数,而在拦截函数内是使用Reflect.get(target, key, receiver)来得到最终结果的,对应到上例,这句话相当于:

Reflect.set(obj, 'bar', receiver)

这其实是实现了通过obj.bar来访问属性值的默认行为。也就是说,引擎内部是通过调用obj对象所部署的[[Get]]内部方法来得到最终结果的,所以有必要通过规范了解[[Get]]内部方法的执行流程,
通过规范可以了解到如果对象自身不存在该属性,那么会获取对象的原型,并调用原型的[[Get]]方法得到最终结果。

对应到上例由于child代理的对象obj自身没有bar属性,因此会获取对象obj的原型,也就是parent对象,所以最终得到的实际上是parent.bar的值。但是parent本身也是响应式数据,因此在副作用函数中访问parent.bar的值时,会导致副作用函数被收集,从而建立响应联系。也就是说child.bar和parent.bar都与副作用函数建立响应联系。
这仍然解释不了为什么设置child.bar的值的时候,会连续触发两次副作用函数执行。所以要了解设置 *** 作发生是的具体执行流程,当执行child.bar = 2时,会调用child代理对象的set拦截函数,在set拦截函数内,使用Reflect来完成默认设置,即引擎会调用obj对象部署的[[Set]]内部方法,根据规范可以知道如果设置的属性不存在对象上,那么会取得其原型,并调用原型的[[Set]]方法。也就是说, *** 作的是child.bar但是也会导致parent代理对象的set拦截函数被执行。而前面也分析到,当读取child.bar的值时,副作用函数会被child.bar收集,也会被parent.bar收集。所以当parent代理对象的set拦截函数执行时,就会触发副作用函数重新执行,所以会导致副作用函数重新执行两次。

那么如何解决这个问题。思路很简单,既然执行两次,那么只要屏蔽其中一次就可以了。所以屏蔽由parent.bar触发的那次副作用函数的重新执行就行。而两次更新是由于set拦截函数被触发了两次导致的,所以只要能够在set拦截函数内区分这两次更新就可以了。

首先要分析设置child.bar的值的过程
设置child.bar的值时,会执行child代理对象的set拦截函数:

// child的set拦截函数
set(target, key, value, receiver){
	// target是原始对象obj
	// receiver是代理对象child
}

可以发现这里receiver其实就是target的代理对象。

但由于obj上不存在bar属性,所以会取得obj的圆形parent, 并执行parent代理对象的set拦截函数

// parent的set拦截函数
set(target, key, value, receiver){
	// target是原始对象proto
	// receiver是代理对象child
}

可以发现此时receiver不再是target的代理对象。
由于最初设置的是child.bar的值,所以无论在什么情况下,receiver都是child,而target是变化的。那么其实只要判断receiver是否是target的代理对象即可。只有当receiver是target的dialing对象时才触发更新,这样就能屏蔽由原型引起的更新。

首先如何确定receiver是不是target的代理对象,这需要给get拦截函数添加一个能力,如下代码:

function reactive(obj){
	return new Proxy(obj, {
		get(target, key, receiver){
			//代理对象可以通过raw属性访问原始数据
			if(key === 'raw'){
				return target
			}
			track(target, key)
			return Reflect.get(target,key,receiver)
		}
		// 省略其他拦截函数
	})
}

这个新功能是:代理对象可以通过raw属性读取原始数据,例如

child.raw === obj // true
parent.raw === proto // true

有了它,就能够在set拦截函数中判断receiver是不是target的代理对象:

function reactive(obj){
	return new Proxy(obj, {
		set(target, key, newVal, receiver){
			const oldVal = target[key]
			const type = Object.prototype.hasOwnProperty.cal(target, key)?'SET','ADD'
			// target === receiver.raw 说明receiver就是target的代理对象
			if(target === receiver.raw){
				if(oldVal !== newVal && (oldVal === oldVAL || newVal === newVal)){
					trigger(target, key, type)
				}
			}
			return res
		}
	})
}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1324571.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存