瞅一眼Vue3源码
地址:https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts
可以看到Proxy响应式代理 依赖 createGetter与createSetter方法:
🚥 createGetter
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 代码忽略...
const res = Reflect.get(target, key, receiver)
// ....
return res
}
}
🚥 createSetter
function createSetter(shallow = false) {
return function set(target: object, key: string | symbol,value: unknown, receiver: object): boolean {
// 代码忽略...
const result = Reflect.set(target, key, value, receiver)
// ....
return result
}
}
理解 Proxy 和 Reflect
既然 Vue3 的响应式数据是基于 Proxy 实现的,那么我们就有必要了解Proxy 以及与之关联的Reflect。什么是 Proxy 呢?简单地说,使用 Proxy 可以创建一个代理对象。它能够实现对其他对象的代理,这里的关键词是其他对象,也就是说,Proxy 只能代理对象,无法代非对象值,例如字符串、布尔值等。那么,代理的是什呢?所谓代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本提作。
什么是基本语义?给出一个对象obj ,可以对它进行一些操作。例如读取属性值、设置属性值:
obj.foo // 读取foo的值
obj.foo++ // 读取和设置foo的值
类似这种读取、设置属性的值的操作,就属于基本语义的操作,既然是基本操作,那么他就可以使用Proxy拦截:
const p = new Proxy(obj, {
// 拦截读取属性
get(){
.......
},
// 拦截设置属性
set(){
.......
}
})
在JavaScript的世界里,万物皆对象。例如一个函数也是一个对象,所以调用函数也是对一个对象基本的操作:
const fn = name=>{
console.log('name==>',name);
}
const p = new Proxy(fn,{
apply(target,thisArg,argArray){
target.call(thisArg,...argArray)
}
})
p('大宝') //输出 name⇒ 大宝
上面两个例子说明了什么是基本操作。Proxy 只能够拦截对一个对象的基本操作。那么,什么是非基本操作呢?其实调用对象下的方法就是典型的非基本操作,我们叫它复合操作:
obj.fn()
实际上,调用一个对象下的方法,是由两个基本语义组成的。第一个基本语义是 get,即先过get操作得到 b.fn属性。第个基本诺义是函数调用,即通过 get 得到 obj.fn 的值后再调用它,也就是我们上面说到的 apply
了解了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.get 读取
Proxy和Relect有什么区别呢?Reflect.get函数还能接收第三个参数者receiver ,可以理解为函数调用过程中的this。
const obj = {
name:'阳了',
get foo(){
return this.name;
}
}
console.log(Reflect.get(obj, 'foo', {name: '阴了'})) // 打印出 '阴了' 而不是 '阳了'
在上段代码中,我们指定第三个参数 recever 为一个对象 {name:“阴了”} ,这时读取到的 receiver 对象的 foo 属性值。实际上, Reflect.* 方法还有很多其他方面的意义,我们只谈论这一点因为它与响应式的实现密切相关。
如果不用Reflect进行映射:
const obj = {foo:1};
const p = new Proxy(obj,{
get(target,key){
track(target,key);
reutrn target[key]
}
set(target,key,newVal){
target[key] = newVal;
trigger(target,key)
}
})
这段代码有什么问题吗?我们借助 effect 让问题暴露出来。首先修改一下obj对象,为它添加bar属性:
const obj = {
foo:1,
get bar (){
return this.foo;
}
}
可以看到,bar属性是一个访问器属性,它返回了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++
副作用函数并没有重新执行,问题出在哪里呢?
实际上,问题就出在 bar 属性的访问器函数 getter 里:上面代码中的return this.foo; this指向哪里?
- 我们回顾一下整个流程首先,我们通过代理对象,访问 p.bar,这会触发代理对象的 get 拦截函数执行
- 在get被拦截函数内,通过 target[key] 返回属性值。其中 target 是原始对象 obj,而key 就是字符串‘bar’, 所以target[key] 相当与obj.bar。因此,当我们使用p.bar访问bar属性时,它的getter函数内的this指向的其实时原始对象obj
- 这说明我们最终访问的是obj.foo.很显然,在 副作用函数 内通过原始对象访问它的某个属性是不会建立响应式联系的,这等价于:
effect(()=>{
// obj 是原始数据,不是代理对象,这样的访问不能够建立响应联系
ob.foo
})
因为这样做不会建立响应式联系,就出现无法触发响应的问题,这时Reflect.get就派上用场了:
const p = new Proxy(obj,{
get(target,key){
// 收集依赖
track(target,key);
// 使用Reflect.get返回读取带的属性的值
reutrn Reflect(target,key,receiver)
}
......
}
})
当我们使用代理对象 p访问 bar 属性时,那么 receiver 就是 p,你可以把它简单地理解数调用中的 this 。
接着关键的一步发生了,我们使用 RefLect.get(target, key, receiver)代替之前的 target[key] ,这的关键点就是第三个参数 receiver。我们已经知道它就是代理象所以访问器属性 bar 的 getter 函数内的 this 指向代理对象 p:
const obj = {
foo:1,
get bar(){
//这里的this指向代理对象p
return this.foo;
}
}
可以看到,this由原始对象变成了代理对象 p。 很显然,这会在在副作用函数与响应式数据之间建立响应联系,从而达到收集依赖的效果。如果此时再对p.foo进行自增操作。会发现已经能够触发副作用函数重新执行了!