proxy代理与反射

83 阅读3分钟

背景

上周分享了对象属性的两种类型,vue2通过相关定义实现了数据劫持,但是这个实现方式有一定的缺陷,比如他无法监听数组的变化,在比如他只能劫持对象的属性,因此实现的时候需要对每个对象的每个属性进行遍历。因此vue3的时候就换了实现方式 Proxy和Reflect。

正文

代理和反射是ES6增加的内容,它为我们提供了拦截并向基本操纵嵌入额外行为的能力。他可以用作目标对象的替身。看概念可能有些抽象,我们可以先实现一个简单的代理。

定义一个简单的代理

ES6给我们提供了Proxy构造函数让我们来创建代理。这个构造函数接受两个必填参数:目标对象和处理程序对象。

new Proxy(target, handler);
/**
* @param target 目标对象;
* @param handler 处理程序对象;
*/

// 目标对象
const target = {
    id: 'target'
};

// 代理对象
const proxy = new Proxy(target, {});

// 这样我们就创建了一个简单的代理.在代理对象上执行任何的操作实际上都会应用到目标对象。

// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target

// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar

// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false

定义捕获器(基本操作的拦截器)

使用代理的主要目的就是可以定义捕获器。我们可以在handler对象里设置0或多个捕获器对对象的各种行为进行捕获拦截; 例如可以定义一个get()捕获器

const target = {
    foo: 'bar',
    id: 'target',
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
    get(trapTarget, property, receiver) {
        if(property === 'id') {
            return undefined;
        }
        return trapTarget[property];
    }
};

const proxy = new Proxy(target, handler);
当通过代理对象执行get()操作时,就会触发定义的get()捕获器(proxy[property]、proxy.property等)进行拦截

console.log(proxy.foo); // bar
console.log(proxy.id); // undefined

get()捕获器可以接收三个参数:目标对象,要查询的属性和代理对象。我们可以基于这三个参数重建原始操作。
但并非所有的捕获器行为都像get()这样简单,每次都要手写代码比较麻烦。ES6给我们提供了Reflect全局对象,通过调用Reflect对象上的同名方法来进行轻松重建。
get(trapTarget, property, receiver) {
    if(property === 'id') {
        return undefined;
    }
    return Reflect.get(...arguments)
}

其他捕获器:

  • set() 支持拦截的操作:proxy[property] = value;proxy.property = value等;
  • has() 支持拦截的操作:property in proxy等;
  • defineProperty() 支持拦截的操作:Object.defineProperty(proxy, property, descriptor)等;
  • deleteProperty() 支持拦截的操作:delete proxy.property; delete proxy[property]等; . . .

常用的使用场景

使用代理我么可以实现一些有用的编程模式。

  • 跟踪属性访问:通过捕获get、set、has等操作,我们可以知道对象属性什么时候被访问,被查询。
  • 隐藏属性: 可以对目标对象上的属性进行隐藏
const hiddenProperties = ['foo', 'bar'];
const targetObject = {
    foo: 1,
    bar: 2,
    baz: 3
};

const proxy = new Proxy(targetObject, {
    get(target, property) {
        if (hiddenProperties.includes(property)) {
            return undefined;
        } else {
            return Reflect.get(...arguments);
        }
    },
    has(target, property) {
        if (hiddenProperties.includes(property)) {
            return false;
        } else {
        return Reflect.has(...arguments);
    }
}

// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log('foo' in proxy); // false
console.log('bar' in proxy); // false
console.log('baz' in proxy); // true

  • 属性验证:因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值:
const target = {
    onlyNumbersGoHere: 0
};
const proxy = new Proxy(target, {
    set(target, property, value) {
        if (typeof value !== 'number') {
        return false;
        } else {
            return Reflect.set(...arguments);
        }
    }
});
proxy.onlyNumbersGoHere = 1;
console.log(proxy.onlyNumbersGoHere); // 1
proxy.onlyNumbersGoHere = '2';
console.log(proxy.onlyNumbersGoHere); // 1