说到 vue3, 总要提到 Proxy, 因为这是vue3较vue2的重要更新/优化内容之一, 好嘛, 这还算能理解, 但最近发现还有一货叫做Reflect, 这又是啥?? 今天就把这俩货捋一捋呗(边查资料边写, 本人的理解或有偏颇, 若有错漏欢迎指出)
打开MDN瞧它一瞧
Proxy(ES6)
简单说就是, 把一个创建的对象传入proxy, 同时把handler传入, 便可通过handler里面定义的方法, 监听对象的操作, 包括: _get, set, apply, defineProperty, deleteProperty_等等, 示例如下:
Tips :
- 这里列出了handler的所有属性方法, 可以只看’get’和’set’,直接跳到到后面看调用
- 也可copy直接调试
- handler属性的定义使用省略属性名的写法, 其方法名即为属性名
const foo = {
user :{},
otherInfo: 'other'
}
// handler的全部属性如下,可以只看'get'和'set',其他先跳过
const handler = {
// @params: target- 目标对象
// @params: property- 将被操作的属性名或Symbol
// @params: value- 新属性值,用于set
// @params: receiver- Proxy或者继承Proxy的对象
get(target,property,receiver){
// 相当于可监控所有属性的读取操作
console.log('get')
// 通过return返回默认行为值或任意值
return property in target ? target[property] : undefined
},
set(target, property, value, receiver){
//读取属性会调用set(),但是需要在本函数中进行赋值,否则该属性不会更新到原对象
console.log('set')
target[property] = value
},
getPrototypeOf(target) {
// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。
// 返回一个对象或null
return Object.getPrototypeOf(target);
},
setPrototypeOf(target, prototype) {
return Object.setPrototypeOf(...arguments)
},
// @params: thisArg- 被调用时的上下文对象,即this
// @params: argumentsList- 被调用时的参数数组
apply(target, thisArg, argumentsList) {
// 拦截函数的调用
return target(...argumentsList)
},
has(target,property){
console.log('has')
return property in target
},
// @params: descriptor- 待定义或修改的属性的描述
defineProperty(target, property, descriptor) {
// 拦截对对象的 Object.defineProperty() 操作
console.log('defineProperty')
Object.defineProperty(...arguments)
// 必须返回一个Boolean以标志是否操作成功
return true
},
deleteProperty(target, property) {
// 拦截对象属性的delete操作
// 必须返回一个Boolean以标志是否操作成功
return true
},
getOwnPropertyDescriptor(target, property) {
return Object.getOwnPropertyDescriptor(...arguments)
},
isExtensible(target) {
return Object.isExtensible(...arguments)
},
ownKeys(target) {
// 用于拦截 Reflect.ownKeys() 或 Object.getOwnPropertyNames()
return ['a', 'b', 'c'];
},
preventExtensions(target) {
// 对Object.preventExtensions()的拦截,返回布尔值
return true
}
}
//使用方法:
const p = new Proxy(foo, handler)
console.log(p.otherInfo)
// > get
// > other
p.otherInfo = 'something else'
console.log(p.otherInfo)
// > set
// > get
// > something else
相信聪明的你已经看出来了, 我们可以在拦截函数中为所欲为😏
除了用实例化(new)的方法进行代理对象的创建, 还可以创建一个可撤销的proxy对象, 看码 :
const bar = {
key: 1
}
let { proxy,revoke } = Proxy.revocable(bar,handler)
console.log(proxy.key)
// > get
// > 1
// 此时调用revoke可以撤销代理
revoke() //撤销代理,后续不能再访问代理对象的属性
console.log(proxy.key)
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
对被代理对象的操作拦截后, 必须手动加入拦截完的处理方法(或默认行为), 否则会影响预期输出
上文代码中也有用到 Object 对应的方法来进行默认行为的操作, 但既有Object为何还要有Reflect?下文详细介绍
Reflect(ES6 为了操作对象而提供的新 API)
Why Reflect? 读完MDN的介绍后其实还是一头雾水
为什么要跟 proxy handler 的命名相同? 为什么又有一些方法与 Object 相同又有一些细微差别???
先解析: *为什么要跟Proxy handler的命名相同? *
在进行对象行为拦截的时候, 当我们不想修改默认行为的时候, 往往需要重复的写一些对象的默认行为的代码, 比如上文代码中有些使用 Object 上已有的方法, 而 Object 没有的方法就用其他方式代替, 而Reflect恰恰有_proxy handlers_ 的所有方法, 那么只要在每一个拦截的方法里面调一个跟handler同名的Reflect的静态方法就可以啦!
比如:
const handler = {
ownKeys(target) {
return Reflect.ownKeys(target)
},
apply(target, thisArg, argumentsList) {
return Reflect.apply(...arguments)
}
//...
}
是不是很棒?
再解释: 为什么又有一些方法与_Object_相同又有一些细微差别???
所谓的细微差别, 不就是大体相同, 小部分不同么?
之所以要有Reflect, 一是为了使一些对象操作的返回值更加合理一些, 比如:
Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,定义成功时返回修改后的对象。
而 Reflect.defineProperty(obj, name, desc) 在定义属性成功时返回 true ,失败时返回 false, 明显更加容易理解, 也更符合常理
二儿一个:
有些 Object 的操作是命令式的, 将其统一转换成函数式, 比如 property in object 和 delete object[property] ,而 Reflect.has(object, property) 和 Reflect.deleteProperty(object, property) 让它们变成了函数行为
Vue3用proxy有啥好处?
相信背过面试题目的同志都知道, vue2 的响应式是用了 Object.defineProperty 对 data 对象进行递归遍历, 把它的所有属性都转换为 getter 和 setter 并在内部追踪其依赖, 当其依赖属性更新时, 去更新界面…
那么vue3流行以后的面试题就变成了大概如 "Vue3用proxy有啥好处? 跟vue2有什么不一样? "
要说Proxy的好处,得先说说 Object.defineProperty 的不足
熟练运用vue2的同志们都知道, 某些情况下我们修改data的时候并不具响应性, 比如:
- 添加一个原本没有的属性
- 通过下标修改数组元素值
因为 Object.defineProperty 实例初始化的时候就调用了, 此时只会把原来已有的属性进行监听, 新加的属性没法监听, 虽说 vue2 也有它的解决方案, 就是 Vue.$set, 但毕竟不够灵活和高效
上文已经介绍过, Proxy的作用是可以对对象的所有操作进行监听, vue3使用proxy便可保证data的所有更新操作都能被正确地监听到, 此处举一个简化版的例子:
// proxy只能监控一层的数据变化,深层次属性读取无法调用set,因此vue3中进行了优化
// 递归读取对象,为每个类型为对象的子属性都创建一个proxy
// 读取子属性时也会触发上一层的get
let bar = {user:{hobbit:'play',food:{fruit: 'apple'}}}
const isObject = (val)=>{
return val !== null && typeof val === 'object'
}
const createProxy = (target)=>{
let p = new Proxy(target,{
get:(target, property)=>{
console.log('get')
let res = target[property] ? target[property] : undefined
// 判断类型,避免死循环
if (isObject(res)) {
return createProxy(res)
} else {
return Reflect.get(...arguments)
}
},
set: (target, property, value)=> {
console.log('set')
return Reflect.set(target, property, value)
}
})
return p
}
let result = createProxy(bar)
result.user.food.fruit = 'pear'
result.user.age = 12
console.log(result)
// { user: { hobbit: 'play', food: { fruit: 'pear' }, age: 12 } }