---
title: 浅谈Proxy           
date: 2022/3/29 12:05:17
tags:                    
- vue3
- proxy
            
author: 张芳利         
---

# Vue3使用的proxy是什么？
众做周知Vue2的双向绑定原理是利用了ES5的一个API`Object.defineProperty()`对数据进行劫持，结合发布订阅模式的方式来实现的。
而Vue3 中则是使用了ES6的Proxy 对数据代理。

<!-- more -->

## Vue2 的Object.defineProperty()
1.  定义：
 Object.defineProperty()方法会直接在一个对象上定义一个新属性，或者修改一个对象的现有属性，并返回此对象。
例子：
```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。



