说说Proxy吧
- 本来是想整理一篇响应式的,但是在写的时候突然感觉对于proxy的形容我只能阿巴阿巴,于是就有了这一篇
一、Proxy
基础了解
- Proxy, 翻译过来就是代理,那么就是起到代替管理的功能
- 创建代理对象的时候,需要指定另外两个对象,目标对象(
target
) 和 处理器对象(handlers
)
let proxy = new Proxy(target, handlers)
- 得到的代理对象是没有自己的状态和行为的,也就是说你对它执行什么操作,它都是只会把操作发送给目标对象或者是处理器对象
- 当处理器对象上存在对应方法,操作就发给处理器对象, 调用该方法执行操作
- 如果处理器对象上不存在对应方法,操作就发给目标对象,那么就在目标对象上执行基础操作
来点🌰
- 先来一个处理器为空的例子,设置了代理对象和目标对象
- 当代理对象
proxy.name
被赋值为字符串dddbug
的时候,目标对象target.name
属性也同时被创建赋值
这里要注意,我们说代理是代替管理,那么就是说代理对象它自身并没有存储该属性,而是将值传递给目标对象去存储。当要获取值时,也是去找目标对象
const target = {}
const proxy = new Proxy(target, {});
//在代理对象上设置值
proxy.name = 'dddbug';
console.log(target.name); //dddbug
console.log(proxy.name); //dddbug
delete proxy.name;
console.log(target.name); //undefined
当然,proxy的功能不可能就这样,那么接下去就继续了解一下我们的处理器对象
二、处理器对象
- 我们将在拦截行为中使用的能够响应特定操作的函数称为陷阱函数
- 每一个陷阱函数都有一个对应的反射方法,是一一对应的
一表直览
陷阱函数 | 反射API | 常规语法 |
---|---|---|
set | Reflect.set() | target[key] = value |
get | Reflect.get() | target[key] |
has | Reflect.has() | key in target |
deleteProperty | Reflect.deleteProperty() | delete target[key] |
getPropertyOf | Reflect.getPropertyOf() | Object.getPropertyOf(target) |
setPropertyOf | Reflect.setPropertyOf() | Object.setPropertyOf(target,p) |
isExtensible | Reflect.isExtensible() | Object.isExtensible(target) |
preventExtensions | Reflect.preventExtensions() | Object.preventExtensions |
getOwnPropertyDescriptor | Reflect.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor |
defineProperty | Reflect.defineProperty() | Object.defineProperty |
apply | Reflect.apply() | f.apply() |
construct | Reflect.construct() | new c() |
ownKeys | Reflect.ownKeys() | Obejct.getOwnPropertyNames() |
我们挑几个常见的出来讲一讲8
Set
- 可以使用陷阱函数
set
来重写设置属性值时的默认行为 -
set(target, key , value ,receiver)
- target:代理的 目标对象
- key: 需要写入的属性的 键
- value: 将被写入属性的值
- receiver: 操作发送的对象(通常是代理对象)
- 🌰光说不用假把式,我们试着用一下
- 这里我对目标对象进行代理,然后分别给代理对象和目标对象设置属性
- 可以看到给目标对象设置属性一切正常
- 而对代理对象设置属性的时候因为我们设置了set陷阱函数,受到拦截,首先是打印了那一句话,然后无论设置什么都会直接赋值为123
const target = {
name: 'dddbug',
}
const proxy = new Proxy(target, {
set(target, key, value, receiver) {
console.log(`我重改了这个方法,我知道你传入的是${value},但是我只想赋值为‘123’`);
target[key] = '123'
return true
}
})
proxy.subName = 'mouche'; //给代理对象赋值
target.nextName = 'clt'; //直接给目标对象赋值
console.log(target.subName); //123
console.log(target.nextName) //clt
get
- 可以使用陷阱函数get来重写获取属性时的默认行为
-
get(target, key , receiver)
- target:代理的 目标对象
- key: 需要获取的属性的 键
- receiver: 操作发送的对象(通常是代理对象)
这里基本跟set一样嘛,不一样的就是get不需要value参数,因为不需要写入数据
- 来个经典🌰,比如说读取对象不存在的属性时我们一般报错为undefined,但是在多数语言中,会抛出错误。那么我们也尝试实现这个效果
- 我们在get内部用in对属性是否在对象上进行了判断,如果不在则抛出错误
- 能够正常读取添加的属性,但是读取不存在的属性则会抛出错误
const proxy = new Proxy({}, {
get(target, key, receiver) {
if(!(key in receiver)) {
throw new TypeError(`Property${key} doesn't exist`);
}
return target[key]
}
})
proxy.name = '123';
console.log(proxy.name)
console.log(proxy.myName)
has
- 可以使用陷阱函数has来重写判断属性时的默认行为
-
has(target, key)
- target:代理的 目标对象
- key: 需要写入的属性的 键
- 我们在表格里面有提到,对应的方法为in,如果对象自身的属性还是其原型的属性与指定的字符串或符号值相匹配,那么就返回true,否则返回false
- 照样上🌰,比如说,我想要隐藏掉某个属性,让代理对象在使用in的时候判断某个存在的属性时输出为false
- 这里我是隐藏了name,就是说,无论我有没有这个属性,统一返回false
const target = {
name : 'mouche',
age: 20
}
const proxy = new Proxy(target, {
has(target, key) {
if(key === 'name') {
return false
} else {
return (key in target)
}
}
})
console.log('name' in proxy); // false
console.log('age' in proxy); //true
deleteProperty
- 可以使用陷阱函数deleteProperty来重写删除属性的默认行为
-
deleteProperty(target, key)
- target: 代理的目标对象
- key: 需要删除的属性
- 比如说,我就是想保护某个属性不被删除,那么就是说不给它执行删除操作
- 这里保护的是name属性,可以看到可以成功删除age属性,但是没有删除name属性
- 🌰大军出发
const target = {
name : 'mouche',
age: 20
}
const proxy = new Proxy(target, {
deleteProperty(target, key) {
if(key === 'name') {
return false
} else {
return delete target[key]
}
}
})
delete proxy.name;
delete proxy.age;
console.log(target)
setPrototype
- 可以通过设置setPrototype陷阱,重写定义对象原型的默认方法
setPrototype(target, proto)
- target: 代理的目标对象
- proto: 需要被用作原型的对象
- 限制了该函数的返回值在没有操作成功的情况下需要返回为false,这样会让
Object.setPrototype()
抛出错误,否则会被认为操作已经完成 - 就是说我们可以让他的原型对象是不可更改的
getPrototype
-
可以通过设置getPrototype陷阱,重写获取对象原型的默认方法
getPrototype(target)
- target: 代理的目标对象
-
Reflect.getPrototype
方法在接收到参数不是一个对象的时候,会抛出错误,而Object.getProptotype
方法则会在获取之前先将参数值转换为对应对象 -
就是说我们可以用它来隐藏他的原型对象
就是说太多了,有兴趣的可以参照上述表格再挨个进行探索,我们是基于Vue3来谈论proxy,那么现在就回归正题,要说到这个肯定要拿Object.defineProperty出来比较一番
三、Object.defineProperty
了解
- MDN文档: Object.defineProperty() - JavaScript | MDN (mozilla.org)
- 语法:
Object.defineProperty(obj, prop, descriptor)
- obj: 要在其上定义属性的对象
- prop: 要定义或修改的属性的名称
- descriptor: 将被定义或修改的属性的描述符
- descriptor的一些属性
- enumerable:属性是否可枚举,默认 false。
- configurable:属性是否可以被修改或者删除,默认 false。
- get:获取属性的方法。
- set:设置属性的方法
使用
- 这里我们写了一个简易的监听对象的过程
defineReactive
函数源码地址:vue/index.js at dev · vuejs/vue · GitHubObserver
类源码地址:vue/index.js at dev · vuejs/vue · GitHub
//使用Observer作为入口监听对象
function Observer(obj) {
//非对象不处理
if(typeof obj !== 'object' || !obj) {
return
}
//通过Object.keys将对象的键列为数组,然后通过forEach遍历,源代码这里采用for循环
Object.keys(obj).forEach(key=> {
//传入函数的值为 监听的对象,键,值
defineReactive(obj, key, obj[key])
})
}
function defineReactive (obj, key, val) {
//因为可能出现对象嵌套对象,如果嵌套的话就将值作为对象传入Observer()
if(typeof val == 'object') {
Observer(val);
}
//如果没有嵌套对象,则对每个属性进行监听
Object.defineProperty(obj,key, {
enumerable: true, //可枚举
configurable: true, //可修改和删除
get: function reactiveGetter() {
//vue2在这里收集依赖
console.log(`你需要获取属性${key}`)
return val;
},
set: function reactiveSetter(newVal) {
console.log(`你在更新属性${key}`)
if(newVal === val) return //如果修改的数据和原数据一样,直接返回
//如果将其修改为对象的话,那么就需要继续给对象所有属性加上监听,于是需要用Observer函数
if(typeof newVal=== 'object') {
Observer(key)
}
//数据更新
val = newVal;
}
})
}
- 写完我们来个嵌套对象测试一下
- 这里我们是要给嵌套对象中的
name
赋值,那么它会先去获取son
,再去获取first
,最后更新name
const test = {
name: 'mouche',
age: 100,
son: {
first: {
name: 'clt',
age: 50,
}
}
}
Observer(test);
test.son.first.name = 'clt11';
直接说说在使用它的过程中我们遇到的问题8
一次只能对一个属性进行监听
- 由于它一次只能监听一个,所以需要通过遍历,递归的方式来对所有的属性进行监听,这个你通过上面冗长的简化代码就知道了
无法优雅处理对象新增删除的属性
- 对象新增或者删除的属性无法被
set
监听到,只有对象本身存在的属性修改才会被劫持 - 原因:vue中数据的初始化initData是在new 实例之后,在created之前的操作,对data绑定一个观察者Observer(obj),所以本身存在的属性修改就会劫持,但是因为对数据绑定Observer时,新增的属性还没有出现,所以也就自然劫持不到
- 所以Vue设计了
$set
和$delete
方法,更新数据的同时手动触发通知依赖
不能监听到数组某些方法的变化
- 无法监听数组长度动态变化
- 不能监听通过监听已有下标的数组元素变化
arr[已有元素下标] = val
,不是因为Object.defineProperty
实现不了,而是其性能代价和用户体验收益不成正比
四、对比
- Proxy是本质上是通过替代管理的操作,在代理对象上执行相关操作,而
Object.defineProperty
是对对象本身 - 其次通过上面的表格就可以知道其实Proxy的代理操作是比较多样化的,而
Object.defineProperty
相对则单调很多只有get
和set
拦截 - 如上述所说,
Object.defineProperty
需要通过遍历,递归的方式来对所有的属性进行监听;而Proxy
只需要直接代理整个对象,当然,如果出现嵌套的对象,Proxy也是需要递归处理的,但是可以做惰性处理,等嵌套对象被需要的时候再对其进行代理 - 如上对于
Object.defineProperty
不能处理的关于对象和数组的监听,Proxy
都可以解决 - 兼容性对比:
- Proxy不支持IE
- Object.property不支持IE8以下
- Proxy不支持IE
转载自:https://juejin.cn/post/7148800472499879972