5.1 理解 Proxy 和 Reflect
非原始值要实现响应式,必须用 Proxy 拦截对象的基本操作;拦截后如何「不改变原始语义地」完成默认行为,则要交给 Reflect。
一、Proxy 代理的是什么
Proxy 用于创建一个代理对象,它能够代理其他对象,关键词是「其他对象」:
- Proxy 只能代理对象,无法代理字符串、布尔值等非原始值之外的原始值(原始值的响应式方案见第 6 章,本章不涉及);
- 它代理的是对一个对象的基本语义操作,允许我们拦截并重新定义这些操作。
基本语义操作指的是类似「读取属性值」「设置属性值」这类操作:
obj.foo // 读取属性 foo 的值 → get
obj.foo++ // 读取并设置属性 foo 的值 → get + set
函数也是对象,所以调用函数同样是对对象的基本操作,可以被 Proxy 的 apply 拦截:
const fn = (name) => {
console.log('我是:', name)
}
// 调用函数是对对象的基本操作
fn()
// 拦截函数的调用操作
const p2 = new Proxy(fn, {
apply(target, thisArg, argArray) {
target.call(thisArg, ...argArray)
}
})
p2('hcy') // 输出:'我是:hcy'
二、基本操作与复合操作
obj.fn() 是典型的非基本操作,也叫复合操作,它由两个基本语义组成:
get:先读取obj.fn属性;apply:得到函数值后调用它。
Proxy 只能拦截基本操作,复合操作要靠多个拦截器组合完成。
三、Reflect:与 Proxy 拦截器同名的默认行为
Reflect 是一个全局对象,它的方法与 Proxy 的拦截器同名:任何能在 Proxy 拦截器中找到的方法,都能在 Reflect 中找到同名函数。
const obj = { foo: 1 }
// 直接读取
obj.foo // 1
// Reflect.get 等价于直接读取,提供的是「默认行为」
Reflect.get(obj, 'foo') // 1
第三个参数 receiver
Reflect.get 还能接收第三个参数,即接收者 receiver,可以把它理解为函数调用过程中的 this:
const obj3 = { foo: 1, get bar() { return this.foo } }
console.log(Reflect.get(obj3, 'bar', { foo: 2 })) // 输出 2 而不是 1
四、为什么要用 Reflect 配合 Proxy
1. 不用 Reflect 会丢失响应联系
访问器属性的 getter 内部用 this.foo 读取时,this 指向谁决定了依赖能否被收集:
const obj = {
foo: 1,
get bar() {
// 这里的 this 指向谁?
return this.foo
}
}
const p = new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key] // 相当于 obj.bar,getter 里的 this 是原始对象 obj
}
})
effect(() => {
console.log(p.bar) // 1,但只收集了 bar 的依赖
})
target[key]中target是原始对象obj,所以 getter 里的this是obj;- 于是访问的其实是
obj.foo,而在副作用函数内通过原始对象访问属性是不会建立响应联系的 → 修改p.foo无法触发副作用函数重新执行。
2. 用 Reflect 传递 receiver 才能正确收集
代理对象的 get 拦截函数接收第三个参数 receiver,它代表谁在读取属性,用代理对象 p 访问时 receiver 就是 p:
const p = new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
// 关键:把 receiver 传给 Reflect,getter 内部的 this 变成代理对象 p
return Reflect.get(target, key, receiver)
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
Reflect.get(target, key, receiver)让访问器属性 getter 内的this由原始对象obj变成代理对象p;- 于是 getter 内
this.foo实际访问的是p.foo,会经过get拦截器执行track,副作用函数与响应式数据之间才建立响应联系。
结论:Proxy 负责「拦截」,Reflect 负责「以正确的
this(receiver)完成默认行为」,二者必须成对使用。
相关
- 5.2 JavaScript 对象及 Proxy 的工作原理:内部方法与 Proxy 拦截器的对应关系,决定了一个操作能不能被代理
- 5.3 如何代理 Object:把 Proxy + Reflect + track/trigger 组合成完整的代理对象