《Vuejs设计与实现》第 5 章(非原始值响应式方案) 上
目录
5.1 理解 Proxy 和 Reflect
5.2 JavaScript 对象和 Proxy 的工作原理
5.3 如何代理 Object
5.1 理解 Proxy 和 Reflect
Proxy 可以创建一个代理对象,实现对其他对象的代理,拦截并重新定义对对象的基本操作。
注意,Proxy 只能代理对象,不能代理非对象值(如字符串、布尔值等)。
基本操作包括读取属性值、设置属性值等。例如:
obj.foo // 读取属性 foo 的值
obj.foo++ // 读取并设置属性 foo 的值
可以使用 Proxy 拦截基本操作:
const p = new Proxy(obj, {// 拦截读取属性操作get() { /*...*/ },// 拦截设置属性操作set() { /*...*/ }
})
Proxy 构造函数接收两个参数:被代理对象和一个包含一组拦截函数的对象(trap夹子)。get 函数用于拦截读取操作,set 函数用于拦截设置操作。
在 JS 中,函数也是对象,所以调用函数也是对一个对象的基本操作:
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') // 输出:'我是:hcy'
Proxy 只能拦截对象的基本操作。 非基本操作,如调用对象下的方法(称为复合操作):
obj.fn()
复合操作实际上由两个基本操作组成的:首先是 get 操作得到 obj.fn 属性,其次是函数调用。即获得 obj.fn 值后再调用它,这就是我们刚才提到的 apply。
理解 Proxy 只能代理对象的基本操作对于后续实现数组或 Map、Set 等数据类型的代理至关重要。
我们来看 Reflect。Reflect 是一个全局对象,提供了一些方法,例如:
- Reflect.get()
- Reflect.set()
- Reflect.apply()
Reflect 中的方法与 Proxy 的拦截器方法同名。它们提供了对象操作的默认行为。例如,以下两个操作是等价的:
const obj = { foo: 1 }// 直接读取
console.log(obj.foo) // 1// 使用 Reflect.get 读取
console.log(Reflect.get(obj, 'foo')) // 1
如果两种操作等价,Reflect 存在的意义是什么呢?
Reflect.get() 还接受第三个参数,也就是 receiver,你可以将它看作函数调用中的 this,例如:
const obj = { foo: 1 }console.log(Reflect.get(obj, 'foo', { foo: 2 })) // 输出的是 2 而不是 1
在这段代码中,我们指定第三个参数 receiver 为一个对象 { foo: 2 },这时读取到的值是 receiver 对象的 foo 属性值。
事实上,Reflect 的各个方法都有很多其他用途,但在此我们只关注与响应式数据实现相关的部分,我们回顾一下上一节的响应式代码:
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.set 完成设置target[key] = newValtrigger(target, key)}
})
在 get 和 set 拦截函数中,我们都是直接使用原始对象 target 来完成对属性的读取和设置操作的,其中原始对象 target 就是上述代码中的 obj 对象。
然而,这段代码存在一些问题。通过 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++
副作用函数并没有重新执行。问题在哪里呢?
实际上,问题出在 bar 属性的 getter 函数里:
const obj = {foo: 1,get bar() {// 这里的 this 指向的是谁?return this.foo}
}
当我们使用 this.foo 读取 foo 属性值时,这里的 this 指向的是谁呢?
我们回顾一下整个流程。首先,我们通过代理对象 p 访问 p.bar,这会触发代理对象的 get 拦截函数:
const p = new Proxy(obj, {