复习一下ES6中的Proxy

980 阅读4分钟

前言

最近一直在复习ES6+的一些新特性,刚好看到Proxy,结合Vue3也是由defineProperty转向了Proxy,因此再好好复习一下Proxy

Proxy是什么

Proxy中文名是代理的意思,它是ES6中提供的创建对象代理的方式,应用目标是对象,这也是Vue3相比Vue2实现响应式原理不同的地方,这个后面会详细说明。Proxy会允许操作者定义额外的行为来拦截或者改写对于目标对象的基本操作,这里说的针对对象的基本操作是指诸如get、set、delete、hasProperty、defineOwnProperty等内部方法,而不是我们常用的比如数组的splice,split,push方法,当然实际上他们内部调用的还是上面这些基本操作。因此Proxy可以在此基础上监听并过滤掉对对象的访问、属性赋值、方法调用等操作。

Proxy的基本使用

// 创建一个Proxy对象
const proxy = new Proxy(target, handler);
  • target: 必选,被代理的目标对象,包括函数,毕竟在JS中函数也是一个对象
  • handler: 必选,一个处理器对象,用于定义各种代理陷阱行为,也就是上述针对对象的一些基本操作

现在尝试创建一个代理陷阱,在这个示例中通过Proxy改写get操作实现了对数组的拦截

const arr = [1, 2, 3]
const proxy = new Proxy(arr, {
    get(target, key) {
        key = Number(key)
        if(key === 2) {
            return 'syx'
        }
        return Reflect.get(target, key)
    },
})

console.log(proxy[2]) // 输出 'syx'

截屏2024-05-26 16.18.58.png 在这个例子中可以很清楚的看到通过Proxy确实是可以改写对象的基本操作,对proxy的a属性进行赋值的时候,不再调用内部的set方法,而是调用我们自定义的set方法(执行打印语句)通过这种方式开发者可以动态的修改对象的各种行为,增强了JS的灵活性和可拓展性。

应用场景

基于Proxy的灵活性,那么在日常开发中Proxy的应用场景有哪些呢?

  • 数据验证与拦截

截屏2024-05-26 16.42.49.png 实际上这是一个典型的动态验证,只有在运行代码执行赋值操作时才会抛出错误,能够统一处理所有属性的赋值校验,而无需在代码中的每个赋值点增加相同的逻辑判断。

  • 反应式编程
const counter = { value: 0 };
const proxyCounter = new Proxy(counter, {
    get(target, prop, receiver) {
        return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
        if (prop === 'value' && value !== target[prop]) {
            console.log('Counter updated from', target[prop], 'to', value);
            // 更新视图或其他副作用
            updateView(value);
        }
        return Reflect.set(target, prop, value, receiver);
     }
});

function updateView(value) {
    // 更新视图的逻辑,例如更新DOM
    console.log('View updated:', value);
}
// 订阅变化
const subscription = () => {
    console.log('Subscription notified');
};
proxyCounter.value = 1; // 触发变化
subscription(); // 手动调用订阅函数通知变化
proxyCounter.value = 2; // 再次触发变化
subscription();

在这个例子中,proxyCounter是一个反应式计数器。每次修改value属性时,set拦截器会检测变化,并调用updateView来更新视图。采用这种方式程序会自动响应数据的变化,和Vue实现双向绑定的原理类似。

Proxy和DefineProperty

我们都知道Vue2升级到Vue3最大的一个特点就是用Proxy代替DefineProperty实现响应式,那他们之间有什么区别呢?Proxy的优势在哪儿呢?

1、Vue2实现响应式

Vue中实现响应式简单来说就是在读取属性和赋值属性的时候,我们要知道它正在读取或者赋值属性,并做一些额外的操作。因此必须要将getset包装成一个函数,在包装的函数中进行一些额外的操作,这就是object.defineProperty所起的作用。

截屏2024-05-26 17.36.06.png Vue2针对的是某个对象的属性的监听,因此在Vue2中必须深度遍历对象中的每个属性。

const obj = {
  a: 1,
  b: 2,
  c: {
    a: 3,
    b: 4,
  },
};

function isObject(obj) {
  return typeof obj === "object" && obj !== null;
}
function obverse(obj) {
  for (const k in obj) {
    let v = obj[k];
    if (isObject(obj[k])) {
      obverse(obj[k]);
    }
    Object.defineProperty(obj, k, {
      get: function () {
        console.log(k, `读取了属性${k}`); // 一些额外操作
        return v;
      },

      set: function (newValue) {
        if (v !== newValue) {
          console.log(k, `更改了属性${k}`); // 一些额外操作

          v = newValue;
        }
      },
    });
  }
}
obverse(obj);
obj.c.a;
obj.c.a = "syx";
console.log(obj.c.a);

c 读取了属性c
a 更改了属性a
c 读取了属性c
a 读取了属性a
syx

在create钩子函数之前就已经完成了监听,obverse的流程已经走完了,因此在vue2无法监听某个属性的新增和删除,因为不会触发gettersetter函数了。此外因为要深度遍历所有的属性,会有性能上的损失。

2、Vue3中使用Proxy的优势 在Vue3中使用了Proxy代替DefineProperty,不再针对属性进行监听,而是直接监听整个对象,也就是开头所提到的target。这样也就不需要进行深度遍历,只有读取到嵌套属性的时候才需要使用到递归操作,所以说使用Proxy后Vue3的性能得到了较大提升,所有的改动都通过Proxy创建的代理对象进行改动。 输出如下:

截屏2024-05-26 18.17.25.png

这样通过Proxy实现响应式不仅带来效率的提升(不需要进行深度遍历),而且对也能监听到新增和删除操作