Commit 7a158021 authored by zhangfangli's avatar zhangfangli

浅谈Proxy

parent 852793e5
Pipeline #15455 canceled with stage
# Vue3使用的proxy是什么?
众做周知Vue2的双向绑定原理是利用了ES5的一个API`Object.definePropert()`对数据进行劫持,结合发布订阅模式的方式来实现的。
而Vue3 中则是使用了ES6的Proxy 对数据代理。
## Vue2 的Object.definePropert()
1. 定义:
Object.definePropert()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
例子:
```jsx
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77;
console.log(object1.property1); //打印出来的一人是42
```
2. 语法:
```jsx
Object.defineProperty(obj, prop, descriptor)
// obj 要定义属性的对象
// 要定义或修改的属性的名称或 Symbol
//descriptor 要定义或修改的属性描述符
```
返回值:被传递给函数的对象。
3. 如何实现响应式的?
通过`defineProperty` 两个属性,get及set
- get
属性的getter函数,当访问改属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承管理,this不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
- set
属性的setter函数,当属性值被修改时,会调用此函数。改方法接受一个参数(也就是被赋的新值),会传入赋值时的this对象。默认undefined.
简单的例子: 监听person 的 namep变化
```jsx
let person ={}
let name = '张小张'
Object.defineProperty(person,'namep',{
// 默认是不可枚举的(for in打印不出来) 可以使用:enumerable:true
// 默认不可修改,可:wirtable:true
//默认不可删除 可:configurable:true
get(){
console.log('触发了get的方法');
return name
},
set(val){
console.log('触发了set的方法');
name = val
}
})
// 读取对象的属性
console.log(person.namep); // 触发了get的方法 张小张
// 修改name,再次读取
name = '王小王'
console.log(person.namep); // 触发了get的方法 王小王
// 修改对象
person.namep = '李小里'
console.log(person.namep); // 触发了set的方法 触发了get的方法 李小里
```
如果监听多个属性的变化,这里就要用到`Object.keys(obj)`进行遍历等,以及深度监听时又要借助递归来实现。
而在监听数组方面
```jsx
let arr = [1,2,3,4,5]
let obj = {}
Object.defineProperty(obj,'arr',{
get(){
console.log('get');
return arr
},
set(val){
console.log('set',val);
arr = val
}
})
console.log(obj.arr); // get [1,2,3,4,5]
obj.arr = [2,3,4] // set [2,3,4]
obj.arr.push(7) // get 但是监听不到下面的了
```
由此看出:数组的一些方法,例如push等,get是监听不到的。
通过索引访问或者修改数组中已经存在的元素,是可以触达get和set,但是对于通过push、unshift增加的元素,会增加索引,这种情况无法监听,通过pop或shift删除元素,会更新索引,也会触发。
在vue2 中,是通过重写Array原型上的方法解决这个问题。
但是其实依然会有很多问题,类似给对象新增一个属性时,也需要手动监听,所以在vue2中给数组或者对象新增属性时,使用this.$set 或者vue.Set()
## Vue3的Proxy
Es6的proxy正好可以解决以上的问题。
1. 定义:
**Proxy** 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的。
2. 语法:
```jsx
const p = new Proxy(target, handler)
//`target` 要使用 `Proxy` 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// `handler` 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 `p` 的行为。
```
简单的例子:
```jsx
// 定义一个需要代理的对象
let person = {
name:'张小张',
age:18
}
//定义handler 对象
let handler = {
get(target,key){ // target就是代理的对象,key 是属性值
return key in target?target[key]:'没有这个属性'
},
set(target,key,val){ // val 是新值
target[key] = val
return true
}
}
let proxyObj = new Proxy(person,handler)
console.log(proxyObj.name) // 张小张
console.log(proxyObj.sex) // 没有这个属性
proxyObj.age = 19
console.log(proxyObj.age) //19
```
这里注意一下:set()方法必须返回一个布尔值,返回 `true` 代表属性设置成功,- 在严格模式下,如果 `set()` 方法返回 `false`,那么会抛出一个异常。
3. Reflect:是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers 的方法相同。
为操作对象而提供的新API,将Object对象的属于语言内部的方法放到Reflect对象上,即从Reflect对象上拿Object对象内部方法。 如果出错将返回false。
所以上面的例子改一下:
```jsx
// 定义一个需要代理的对象
let person = {
name:'张小张',
age:18
}
//定义handler 对象
let handler = {
get(target,key){ // target就是代理的对象,key 是属性值
console.log(Reflect.get(target, key));
return Reflect.get(target, key)?Reflect.get(target, key):'没有这个属性'
},
set(target,key,val){ // val 是新值
Reflect.set(target,key,val)
return true
}
}
let proxyObj = new Proxy(person,handler)
console.log(proxyObj.name) // 张小张 张小张
console.log(proxyObj.sex) // undefined 没有这个属性
proxyObj.age =22
console.log(proxyObj.age) //22
```
用这个例子和上面的例子作比较发现:Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。
不光如此,对于上面遇到的数组方法、对象新增属性等都可以得到解决。
4. Proxy 的拦截:
除了上面用到的set()和get(),Proxy支持的拦截操作一共有13种。具体信息可以查看MDN([https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy]
- get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']
- set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
- has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
- deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
- setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
5. Proxy 总结:
Proxy相比Object.defineProperty,可以直接监听整个对象而非属性,也可以直接监听数组的变化,并且提供13中拦截方法。但是缺点是不兼容IE,存在浏览器兼容问题,也没有polyfill(指的是一个代码块。这个代码块向开发者提供了一种技术, 这种技术可以让浏览器提供原生支持,抹平不同浏览器对API兼容性的差异)。Object.defineProperty支持IE9。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment