◆

Vue3 响应式实现:Proxy 与 defineProperty 对比

2023-02-02 01:370

如下代码

我们创建了一个 effect 函数用来计算 total 的值,我们在每次打印 total 之前都调用了 effect 函数,所以当 product 变化时,我们能获取到 total 当时的值。 但是不能每一次 product 发生变化我们都手动去触发 effect 函数去获取最新的值吧,这样显得十分繁琐,如何去解决这个问题呢?

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <script>
      let product = {
        price: 10,
        quantity: 2,
      };

      let total = 0;

      let effect = () => {
        // 在 effect 计算时,我们获取了 product 中属性的值,这是 getter 行为
        total = product.price * product.quantity;
      };
      effect();
      console.log('总价格:' + total);
      // 修改 quantity 属性,这是 setter 行为
      product.quantity = 5;
      effect();
      console.log('总价格:' + total);
    </script>
  </body>
</html>

使用 Object.defineProperty 去解决

如下我们使用了 Object.defineProperty 对 product 的 quantity 属性进行了代理。当 quantity 发生变化的时候会触发 setter 并且更新 quantity 的值,然后执行 effect。 通过下面这种方法可以实现,每次修改 product 的 quantity 属性的值,都会自动触发 effect

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <script>
      let product = {
        price: 10,
        quantity: 2,
      };
      Object.defineProperty(product, 'quantity', {
        set(newval) {
          console.log('触发 setter', newval);
          quantity = newval;
          effect();
        },
        get() {
          console.log('触发 getter');
          return quantity;
        },
      });

      let total = 0;

      let effect = () => {
        total = product.price * product.quantity;
      };
      console.log('总价格:' + total);

      product.quantity = 5;
      console.log('总价格:' + total);
    </script>
  </body>
</html>

Object.defineProperty 的问题

虽然 Object.defineProperty 可以通过 getter 与 setter 实现响应式,但是这样只能监听到数据的修改操作,当我们监听的对象有新增或者删除属性时 Object.defineProperty 并不能实现相应式变化。

这种问题在 vue2 中都是通过一些特殊的 hack 手段才能实现响应式,如对对象整体重新赋值,或者调用 vue 提供的特殊 api 去实现

vue3 解决响应式的方法

proxy

如下代码所示,我们使用 proxy 取代了 Object.defineProperty

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <script>
      let product = {
        price: 10,
        quantity: 2,
      };
      const proxyProduct = new Proxy(product, {
        set(target, key, newval, recevier) {
          console.log('触发 setter', target, key, newval);
          target[key] = newval;
          effect();
        },
        get(target, key, recevier) {
          console.log('触发 getter', target, key);
          return target[key];
        },
      });

      let total = 0;

      let effect = () => {
        total = proxyProduct.price * proxyProduct.quantity;
      };
      console.log('总价格:' + total);

      proxyProduct.quantity = 5;
      console.log('总价格:' + total);
    </script>
  </body>
</html>

综上所述,可以看出 Object.defineProperty 与 proxy 区别如下:

proxy

  1. proxy 将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。
  2. 当想要修改对象的指定属性时,我们应该使用代理对象进行修改
  3. 代理对象的任何一个属性都可以触发 handler 的 getter 和 setter

Object.defineProperty

  1. Object.defineProperty 为指定对象的指定属性设置属性描述符。
  2. 当想要修改对象的指定属性时,可以使用原对象进行修改
  3. 通过属性描述符,只有被监听的指定属性, 才可以触发 getter 和 setter

所以当 vue3 通过 proxy 实现响应性 api 之后,vue 将不会在存在新增属性时失去响应性的问题。

使用 Reflect 配合 proxy 改变 this 指向问题

如下代码,在被代理对象内容通过 this 触发 getter 与 setter 时,需要使用 Reflect.get(target, key, recevier) 去获取,否则,当前的 this 对象依旧是被代理对象

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <script>
      let p1 = {
        firstName: 'w',
        lastName: 'fly',
        get fullName() {
          return this.firstName + this.lastName;
        },
      };
      const proxy = new Proxy(p1, {
        get(target, key, recevier) {
          console.log('触发 getter');
          return target[key];
        },
      });
      // 只会触发一次,这时 get 中的 this 指向 p1
      console.log(proxy.fullName);

      let p2 = {
        firstName: 'w',
        lastName: 'fly',
        get fullName() {
          return this.firstName + this.lastName;
        },
      };
      const proxy2 = new Proxy(p1, {
        get(target, key, recevier) {
          console.log('触发 getter', recevier);
          return Reflect.get(target, key, recevier);
        },
      });
      // 会触发三次,这时 get 中的 this 指向 proxy2
      console.log(proxy2.fullName);
    </script>
  </body>
</html>
💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue3 响应式实现:Proxy 与 defineProperty 对比加载中