阅读前提
对Js与Vue有最基本的了解,包括但不限于:
Object.defineProperty与Proxy的区别与使用- Vue的响应式系统的作用与实现,需要了解其中的数据结构、副作用函数等 如果对以上前置知识不太了解,可能阅读起来会有难度,本文会以最简单化形式呈现。
标题在说什么问题
首先是两个概念的理解:
Proxy:简单地说,使用 Proxy 可以创建一个代理对象。它能够实现对其他对象的代理,这里的关键词是其他对象,也就是说,Proxy 只能代理对象,无法代理非对象值,例如字符串、布尔值等。代理:指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。 哪里用到了Reflect? 以下是对Vue源码中使用Proxy代理时涉及get的一个最简单的写法
const p = new Proxy(obj, {
// 拦截读取操作,接收第三个参数 receiver
get(target, key, receiver) {
track(target, key)
// 使用 Reflect.get 返回读取到的属性值
// 为什么不用 target[key] ?
return Reflect.get(target, key, receiver)
},
// 省略部分代码
})
p.name // 此时会触发get函数
但是,如果对getter函数有过了解的小伙伴会问:为什么Vue不直接使用target[key]进行返回?
这就是这个标题提出的疑惑,也是本文想探讨的内容。
在解决这个问题之前,首先需要简单回顾Vue的响应式数据和副作用函数是啥
响应式与副作用函数
实现响应式
假设我们有一个原始对象obj
const obj = {
name: 'lukas',
age: 18,
}
我们要监视这个对象的操作,如何做呢?例如:
obj.name = 'jay'
在js内部一定会设计一套操作能够得知:
obj是否有一个name属性,如果有,那么name的值是多少- 新值
'jay'是否是合理的值,如果合理,那么obj.name设置为新值'jay'至少在这一步上,js对于对象会有我们熟知的getter、setter拦截 那么js不妨把一些对象内部的接口暴露出来给程序员使用即可,就有了Object.defineProperty,就有了Proxy。这两个也是Vue2、Vue3实现响应式的主要方式。(二者的区别不在本文提及)
Vue实现响应式
Vue借助Proxy的对一个对象进行拦截的读取和设置操作后,便可以实现响应式,例如:
// 存储副作用函数的桶
const bucket = new Set()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用函数的桶中
bucket.add(effect)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
bucket.forEach(fn => fn())
// 返回 true 代表设置操作成功
return true
}
})
// 副作用函数
function effect() {
document.body.innerText = obj.text
}
// 执行副作用函数,触发读取
effect()
// 1 秒后修改响应式数据
setTimeout(() => { obj.text = 'hello vue3' }, 1000)
- 初次
effect走到obj.text的时候触发Set操作,将effect存在桶中(实际数据结构会比这个更复杂:使用weakMap的key放置对象,使用map的key放置对象的属性值,使用set放置依赖副作用函数,不在本文提及) - 在最后一行定时器改变值之后,会触发
obj对于text的Set操作,并重新执行effect函数 以上是对Vue响应式和副作用函数的最简单的实现(具体优化与实现不在本文中提及)
所以为什么使用Reflect
回顾完vue的响应式和副作用函数后,回到本文最开始的代码,这里的返回值用了Reflect
const p = new Proxy(obj, {
// 拦截读取操作,接收第三个参数 receiver
get(target, key, receiver) {
track(target, key)
// 使用 Reflect.get 返回读取到的属性值
return Reflect.get(target, key, receiver)
},
})
p.name // 此时会触发get函数
什么是Reflect
Reflect:是一个全局对象,其下有许多方法
Reflect.get()
Reflect.set()
Reflect.apply()
// ...
- Reflect 下的方法与 Proxy 的拦截器方法名字相同,任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数
- 那么这些函数的作用是什么呢?拿
Reflect.get函数来说,它的功能就是提供了访问一个对象属性的默认行为 Reflect.get函数还能接收第三个参数,即指定接收者receiver,你可以把它理解为函数调用过程中的this
const obj = { foo: 1 }
// 直接读取
console.log(obj.foo) // 1
// 使用 Reflect.get 读取
console.log(Reflect.get(obj, 'foo')) // 1
console.log(Reflect.get(obj, 'foo', {foo: 2})) // 2
使用target[key]的弊端
在这种场景下,副作用函数并不会重新执行
const obj = {
foo: 1,
get bar() {
return this.foo
}
}
const p = new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
// 返回属性值
return target[key]
},
})
effect(() => { console.log(p.bar) }) // 1
p.foo++
这里做了什么呢
- 为原始对象
obj设了一个代理对象p - 副作用函数注册时,读取到
p.bar,发现其为一个访问器属性,于是执行getter函数,返回了this.foo - 这里的this是谁呢?
p.bar的返回值target[key]其实就是obj.bar(get函数的参数target:obj,key:'bar')所以这里的this指向的是原始对象obj - 所以副作用函数读取到的是
obj.foo,副作用函数跟原始对象obj响应上了,但是我们是通过代理对象p建立响应关系的呀~ - 所以我们
p.foo++的时候,p并不知道要让这个副作用函数再次执行,所以effect不会重新执行,控制台没有新变化
Reflect.*方法响应代理对象
那该咋让副作用函数和代理对象p响应上呢?把代理对象的get修改一下
const obj = {
foo: 1,
get bar() {
return this.foo
}
}
const p = new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
// 返回属性值
return Reflect.get(target, key, receiver)
},
})
effect(() => { console.log(p.bar) }) // 1
p.foo++ // 传入的receiver就是p这个对象
代理对象的 get 拦截函数接收第三个参数 receiver,它代表谁在读取属性
改成Reflect的话这里做了什么呢
- 为原始对象
obj设了一个代理对象p - 副作用函数注册时,读取到
p.bar,发现其为一个访问器属性,于是执行getter函数,返回了this.foo - 这里的this是谁呢?我们已经知道
receiver是代理对象p,所以访问器属性bar的getter函数内的this指向代理对象p - 所以副作用函数读取到的是
p.foo,副作用函数跟代理对象p响应上了 - 所以我们
p.foo++的时候,p知道要让这个副作用函数再次执行(set函数的作用),所以effect重新执行,在控制台输出2 - 本文标题问题得到答案
Proxy对象部署的所有内部方法
在其他的拦截操作中也能见到Reflect的影子
const p = new Proxy(obj, {
// delete操作
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key)
},
// in操作
has(target, key) {
return Reflect.has(target, key)
}
// ...
})
创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来指定被代理对象的内部方法和行为的。
参考书籍
这是我在阅读《Vue.js设计与实现》第4、5章时因疑惑整理留下的一份笔记,供大家共同学习与讨论更正。