【vue设计与实现】非原始值的响应式方案 1-理解Proxy和Reflect

本章要把目光聚集在响应式数据本身,深入探讨实现响应式数据都需要考虑那些内容,其中的难点又是什么?实际上,想要实现完善的响应式数据,需要深入语言规范。

什么是Proxy

简单说,使用Proxy可以创建一个代理对象。它能够实现对其他对象的代理,也就是说Proxy只能代理对象,无法代理非对象值;而代理指的是对一个对象基本语义的代理。它允许我们拦截重新定义对一个对象的基本操作。

基本语义指的是对对象进行的一些操作,例如读取属性值,设置属性值等,例如:

obj.foo // 读取属性foo的值
obj.foo++ //读取和设置foo的值

这样的操作就可以通过Proxy来拦截

const p = new Proxy(obj, {
	// 拦截读取属性操作
	get(){ /*...*/}
	// 拦截设置属性操作
	set(){ /*...*/}
})

在JavaScript里,函数也是一个对象,所以调用函数也是对一个对象的基本操作

const fn = (name) => {
	console.log('我是:'+ name)
}
// 调用函数时对对象的基本操作
fn()

所以我们可以用Proxy来拦截函数的调用操作,这里使用apply拦截函数的调用

const p2 = new Proxy(fn, {
	// 使用apply拦截函数调用
	apply(target, thisArg, argArray){
		target.call(thisArg, ...argArray)
	}
})

p2('hcy')

那么什么是非基本操作?其实调用对象下的方法就是典型的非基本操作,我们叫它复合操作:

obj.fn()

*调用一个对象下的方法,由两个基本语义组成:第一个是基本语义get,即先通过get得到obj.fn属性,第二个基本语义是函数调用,即调用方法,也就是上面说到的apply。

有关Proxy的详细介绍:https://es6.ruanyifeng.com/#docs/proxy

什么是Reflect

Reflect是一个全局对象,下面有许多方法,例如:

Reflect.get()
Reflect.set()
Reflect.apply()

可以看到Reflect下的方法雨Proxy的拦截器方法名字相同,其实任何在Proxy中能找到的方法,都能够在Reflect中刚找到同名函数,关于这些函数的作用,拿Reflect.get函数来说,其功能就是提供了访问一个对象属性的默认行为。例如下面两个操作是等价的:

const obj = {foo:1}

console.log(obj.foo) //1
console.log(Reflect.get(obj, 'foo')) //1

其中Reflect不同的点在于还可以接收第三个参数,即指定接受者receiver,可以把它理解为调用过程中的this,例如:

const obj = {foo:1}
console.log(Reflect.get(obj,'foo', {foo:2})) //输出是1
// 在 Reflect.get(obj, 'foo', {foo: 2}) 这行代码中,Reflect.get() 方法的第三个参数是用来指定函数调用时的 this 值。但是,对于 Reflect.get() 方法来说,第三个参数并不会影响属性的获取。
// 如果你想要将 obj 对象的 'foo' 属性值修改为 {foo: 2} 中的 foo 属性值,你需要使用 Reflect.set() 方法,如下所示:
const obj = {foo: 1};
Reflect.set(obj, 'foo', Reflect.get({foo: 2}, 'foo'));
console.log(obj.foo); // 输出为 2

这里指定了第三个参数receiver为对象{foo:2},这是读取到的值是receiver对象的foo属性值。

这里先回顾下之前提到的实现响应式数据的代码

const obj = {foo:1}
const p = new Proxy(obj,{
	get(target, key){
		track(target,key)
		// 注意,这里我们没有使用Reflect.get完成读取
		return target[key]
	}
	set(target,key,newVal){
		// 这里同样没有使用Reflect.get完成读取
		target[key] = newVal
		trigger(target,key)
	}
})

那么这段代码有什么问题吗?可以借助effect让问题暴露出来,首先修改obj对象,为其添加bar属性

const obj = {
	foo:1,
	get bar(){
		return this.foo
	}
}

接着在effect副作用函数中通过代理对象p访问bar属性

effect(()=>{
	console.log(p.bar) //1
})

分析一下这个过程发生了什么。当effect注册的副作用函数执行时,会读取p.bar属性,它发现p.bar是一个访问器属性,因此执行getter函数。由于在getter函数中通过this.foo读取了foo属性值,因此我们认为副作用函数与属性foo之间也会建立联系。那么修改p.foo的值时应该能够触发响应,使得副作用函数重新执行才对。然而实际并非如此,当我们尝试修改p.foo的值时:

p.foo++

副作用函数并没有重新执行,问题出在哪里?
实际上问题就出在那个this上:

const obj = {
	foo:1,
	get bar(){
		// 这里的this指向的是谁?
		return this.foo
	}
}

那么这里我们回顾一下整个流程。首先,通过代理对象p访问p.bar,这会触发代理对象的get拦截函数执行。在get拦截函数内,通过target[key]返回属性值。其中target是原始对象obj,而key就是字符串bar,所以target[key]相当于obj.bar。因此当我们使用p.bar访问bar属性时,它的getter函数内的this指向的其实时原始对象obj,这时我们最终访问的是obj.foo。显然在副作用函数内通过原始对象访问它的某个属性是不会建立响应联系的。这时Reflect.get函数就派上用场了。先给出解决问题的代码:

const p = new Proxy(obj, {
	get(target, key, receiver){
		track(target, key)
		// 使用 Reflect.get返回读取到的属性值
		return Reflect.get(target, key, receiver)
	}
})

上面代理对象的get拦截函数接受第三个参数receiver, 它代表谁在读取属性,例如:

p.bar // 代理对象p在读取bar属性

所以访问器属性bar的getter函数内的this指向代理对象p

const obj = {
	foo:1,
	get bar(){
		// 这里的this为代理对象p
		return this.foo
	}
}

这样就会在副作用函数雨响应数据之间建立响应联系。如果此时再对p.foo进行自增操作,会发现已经能够触发副作用函数重新执行。
因此后面将统一使用Reflect.*方法

有关Reflect的详细介绍:https://es6.ruanyifeng.com/#docs/reflect

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值