非原始值的响应式方案
先看下面实例代码
const obj = {
foo: 1,
get bar() {
return this.foo
}
}
const p = new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key);
}
})
effect(() => {
console.log(p.bar) // 1
})
p.foo++;
执行p.foo++后,副作用函数并没有重新执行。原因是:
当effect副作用函数执行时,会读取p.bar,p.bar是一个访问器属性,因此执行getter函数,getter函数中返回了this.foo。此时的this指向的是谁呢?
回顾一下整个流程:首先通过代理对象p访问p.bar,这回触发代理对象p的get拦截函数,拦截函数返回target[key]
,target是原始对象obj,key是bar,所以target[key]
相当于obj.bar
。又因为obj.bar是访问器属性,他的this指向的是原始对象obj。这说明最终访问的其实是obj.foo,等价于
effect(() => {
obj.foo
})
在副作用函数内,通过原始对象访问他的某个属性是不会建立响应联系的
那个整个问题应该如何解决呢?
Reflect
const p = new Proxy(obj, {
get(target, key, receiver){
track(traget, key)
return Reflect.get(target, key, receiver)
}
})
代理对象get拦截函数接收的第三个参数receiver,代表了谁在调取属性。
p.bar // 代理对象p在读取bar属性
const obj = {
foo: 1,
get bar() {
// 现在这里的this,指向的就是代理对象p
return this.foo
}
}
Proxy工作原理
根据ECMAscript规范,js中有两种对象:常规对象、异质对象。
在js中函数也是对象,给出一个obj,如何区分他是普通对象还是函数?在js中,对象的实际语义是由对象的内部方法指定的。所谓内部方法,指的是当我们对一个对象进行操作时在引擎内部调用的方法,这些方法对js使用者不可见。
obj.foo // 引擎内部调用[[Get]]整个内部方法来读取属性。
js的内部方法 11 + 2个必要方法
由此可知,如何区分普通对象和函数:如果对象是一个函数,那个这个对象必须部署内部方法[[Call]],但普通函数不会。
常规对象:
- 满足必要的内部方法,且必须使用ECMA规范给出定义实现
- 对于内部方法[[Call]] [[Construct]]必须使用ECMA规范定义实现
不满足以上条件的均为异质对象。
Proxy就是异质对象,Proxy内部[[Get]]没使用标准定义实现。Proxy中如果没有指定get()拦截函数,那就通过原始[[Get]]方法获取值。
Proxy:创建代理对象时指定了get拦截函数,用来自定义代理对象本身的内部方法和行为。
除了get以外,还有[[Set]] [[Delete]] [[GetPrototypeOf]] [[SetPrototypeOf]] [[HasProperty]]。。。等多个内部方法。
// 代理内部删除属性
const p = new Proxy(obj, {
deleteProperty(target, key){
return Reflect.deleteProperty(target, key)
}
})
原始值响应方案ref
原始值是按值传递的,而非按引用传递。一个函数接受原始值作为参数,形参和实参之间没有引用关系。
针对这个问题,对原始值做一层包裹,变成响应式数据
function ref(val){
const wrapper = {
value: val
}
return reactive(wrapper)
}
总结
Vue.js3的响应式数据是基于Proxy实现的,Proxy可以为其他对象创建一个代理对象。所谓代理,指的是对一个对象的基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。在实现代理的过程中,会遇到访问器属性this的指向问题,通过使用Reflect.* 方法并指定正确的receiver来解决。