Vue 3 响应式原理二 - Proxy and Reflect
在上一篇【Vue 3 响应式原理一 - Vue 3 Reactivity】
中,我们知道了 Vue 3 如何跟踪effects
,以便在需要时重新运行它们。然而,我们仍然需要手动调用track
和trigger
。现在我们将学习如何使用Reflect
和Proxy
来自动调用它们。
Hooking onto Get and Set
我们需要一种方法来 hook (或侦听) 我们的响应式对象上的get
和set
。
GET property (访问属性) => 调用track
去保存当前 effect
SET property (修改了属性) => 调用trigger
来运行属性的 dependencies (effects)
如何做到这些?在 Vue 3 中我们使用 ES6 的Reflect
和Proxy
去拦截 GET 和 SET 调用。Vue 2 中是使用 ES5 的Object.defineProperty
实现这一点的。
理解 ES6 Reflect
要打印出一个对象的某属性可以像这样做:
let product = { price: 5, quantity: 2 }
console.log('quantity is ' + product.quantity)
// or
console.log('quantity is ' + product['quantity'])
然而,也可以使用Reflect
去 GET 对象上的值。 Reflect
允许你用另一种方式获取对象的属性:
console.log('quantity is ' + Reflect.get(product, 'quantity'))
为什么使用reflect
?因为它具有我们稍后需要的特性,在理解 ES6 Proxy 之后再来展示。
理解 ES6 Proxy
Proxy 是另一个对象的占位符,默认情况下对该对象进行委托。 如果我运行如下代码:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {})
console.log(proxiedProduct.quantity)
注意到 Proxy 的第二个参数{}
了吗?这是一个handler
,可用于定义代理对象(Proxy)上的自定义行为,例如拦截 get 和 set 调用。这些拦截器方法称为traps
(捕捉器),可以帮助我们拦截一些基本操作,如属性查找、枚举或函数调用。下面是如何在handler
上设置 get traps
:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get() {
console.log('Get was called')
return 'Not the value'
}
})
console.log(proxiedProduct.quantity)
我们应该返回实际的值,像这样:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key) { // <--- The target (代理的对象) and key (属性名)
console.log('Get was called with key = ' + key)
return target[key]
}
})
console.log(proxiedProduct.quantity)
image.png
get 函数有两个参数,target
是我们的对象(product
)和我们试图获取的key
(属性),在本例中是quantity
。
当我们在 Proxy 中使用Reflect
,可以添加一个额外参数,可以被传递到Reflect
调用中。
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) { // <--- notice the receiver
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver) // <----
}
})
这能确保当我们的对象有从其他对象继承的值/函数时,this 值能正确地指向调用对象。使用 Proxy 的一个难点就是this
绑定。我们希望任何方法都绑定到这个 Proxy,而不是target
对象。这就是为什么我们总是在Proxy
内部使用Reflect
,这样我们就能保留我们正在自定义的原始行为。
现在让我们添加一个setter
方法:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) {
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver)
}
set(target, key, value, receiver) {
console.log('Set was called with key = ' + key + ' and value = ' + value)
return Reflect.set(target, key, value, receiver)
}
})
proxiedProduct.quantity = 4
console.log(proxiedProduct.quantity)
set 除了使用Reflect.set
接收值来设置 target 之外,看起来与 get 非常相似。输出也符合我们的预期。
我们可以通过另一种方式封装这段代码,就像在 Vue 3 源码中看到的那样。首先,我们将这个代理委托代码包装在一个返回proxy
的响应式函数中,如果你用过 Vue 3 Composition API,它应该看起来很熟悉。然后将包含 get 和 set traps 的handler
常量发送到我们的proxy
中。
function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('Set was called with key = ' + key + ' and value = ' + value)
return Reflect.set(target, key, value, receiver)
}
}
return new Proxy(target, handler) // 创建一个 Proxy 对象
}
let product = reactive({ price: 5, quantity: 2 }) // <-- Returns a proxy object
product.quantity = 4
console.log(product.quantity)
这会返回与上面相同的结果,但现在我们可以轻松地利用reactive
方法创建多个响应式对象。
结合 Proxy + Effect 存储
回到最初的起点:
GET property (访问属性) => 我们需要调用track
去保存当前 effect
SET property (修改了属性) => 我们需要调用trigger
来运行属性的 dependencies (effects)
track 将检查当前运行的是哪个副作用(effect),并将其与 target 和 property 记录在一起。这就是 Vue 如何知道这个 property 是该副作用的依赖项。
我们可以想象一下上面的reactive
代码,需要调用 track
和 trigger
的地方。
思路整理:
-
当一个值被读取时进行追踪:proxy 的
get
处理函数中track
函数记录了该 property 和当前副作用。 -
当某个值改变时进行检测:在 proxy 上调用
set
处理函数。 -
重新运行代码来读取原始值:
trigger
函数查找哪些副作用依赖于该 property 并执行它们。
直接整上:
const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
function track(target, key) {
// We need to make sure this effect is being tracked.
let depsMap = targetMap.get(target) // Get the current depsMap for this target
if (!depsMap) {
// There is no map.
targetMap.set(target, (depsMap = new Map())) // Create one
}
let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
if (!dep) {
// There is no dependencies (effects)
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(effect) // Add effect to dependency map
}
function trigger(target, key) {
const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
if (!depsMap) {
return
}
let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
if (dep) {
dep.forEach(effect => {
// run them all
effect()
})
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
// Track
track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue != result) {
// Trigger
trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
}
return result
}
}
return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
total = product.price * product.quantity
}
effect()
console.log('before updated quantity total = ' + total)
product.quantity = 3
console.log('after updated quantity total = ' + total)
这段代码输出:
before updated quantity total = 10
after updated quantity total = 15
现在我们不再需要调用trigger
和track
,因为它们在我们的get
和set
方法中被合理地调用。
使用 Proxy 和 Reflect 能带来什么好处?
当你使用
proxies
时,也就是所谓的响应式转换,是懒执行的。
而把对象传给 Vue 2 的响应式时,则必须遍历所有的 key,并且当场转换,以确保它们被访问时都是响应式的。
对于 Vue3,当调用reactive
时,返回的是一个proxy
代理对象,并且只会在需要的时候才去转换嵌套的对象。有点像"懒加载"。这样做的好处打个比方,当你进行分页渲染,那么只有第一页需要的10个object
需要经过响应式转化。这对应用程序而言可以节省很多时间,特别是当程序拥有庞大的列表对象时。
我们已经前进了一大步!在此代码稳固之前,只有一个 bug 需要修复。具体来说,我们只希望track
在 响应式对象有被effect
使用 时才被调用。现在只要响应式对象属性是get
,就会调用track
。我们将在下一篇中完善这一点。
Vue 3 响应式原理一 - Vue 3 Reactivity
Vue 3 响应式原理二 - Proxy and Reflect
Vue 3 响应式原理三 - activeEffect & ref
Vue 3 响应式原理四 - Computed Values & Vue 3 源码