• 带你深入领略 Proxy 的世界


    Proxy 是 es2015 标准规范加入的语法,很可能你只是听说过,但并没有用过,毕竟考虑到兼容的问题,不能轻易地使用 Proxy 特性。

    但现在随着各个浏览器的更新迭代,Proxy 的支持度也越来越高:

    Proxy的浏览器支持程度

    而且使用 Proxy 进行代理和劫持,也渐渐成为了趋势。Vue3 已经用 Proxy 代替了 Object.defineProperty 实现响应式,mobx 也从 5.x 版本开始使用 Proxy 进行代理。

    1. Proxy 的基本结构

    Proxy 的基本使用方式:

    /**
     * target: 表示要代理的目标,可以是object, array, function类型
     * handler: 是一个对象,可以编写各种代理的方法
     */
    const proxy = new Proxy(target, handler);
    

    例如我们想要代理一个对象,可以通过设置 get 和 set 方法来代理获取和设置数据的操作:

    const person = {
      name: 'wenzi',
      age: 20,
    };
    const personProxy = new Proxy(person, {
      get(target, key, receiver) {
        console.log(`get value by ${key}`);
        return target[key];
      },
      set(target, key, value) {
        console.log(`set ${key}, old value ${target[key]} to ${value}`);
        target[key] = value;
      },
    });
    

    Proxy 仅仅是一个代理,personProxy 上有 person 所有的属性和方法。我们通过personProxy获取和设置 name 时,就会有相应的 log 输出:

    personProxy.name; // "wenzi"
    // log: get value by name
    
    personProxy.name = 'hello';
    // log: set name, old value wenzi to hello
    

    并且通过 personProxy 设置数据时,代理的原结构里的数据也会发生变化。我们打印下 person,可以发现字段 name 的值  也变成了hello

    console.log(person); // {name: "hello", age: 20}
    

    世界的参差增加了

    Proxy 的第 2 个参数 handler 除了可以设置 get 和 set 方法外,还有更多丰富的方法:

    • 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)。

    如我们通过 delete 删除其中一个元素时,可以通过deleteProperty()方法来拦截这个操作。还是上面代理 person 的代码,我们添加一个 deleteProperty:

    const person = {
      name: 'wenzi',
      age: 20,
    };
    const personProxy = new Proxy(person, {
      // 忽略get和set方法,与上面一样
      // ...
      deleteProperty(target, key, receiver) {
        console.log(`delete key ${key}`);
        delete target[key];
      },
    });
    

    当执行 delete 操作时:

    delete personProxy['age'];
    // log: delete key age
    

    2. Proxy 与 Reflect

    Proxy 与 Reflect 可以说形影不离了,Reflect 里所有的方法和使用方式与 Proxy 完全一样。

    例如上面 Proxy 里的 get(), set()和 deleteProperty()方法我们都是直接操作原代理对象的,这里我们改成使用Reflect来操作:

    const personProxy = new Proxy(person, {
      get(target, key, receiver) {
        console.log(`get value by ${key}`);
        return Reflect.get(target, key, receiver);
      },
      set(target, key, value, receiver) {
        console.log(`set ${key}, old value ${target[key]} to ${value}`);
        return Reflect.set(target, key, value, receiver);
      },
      deleteProperty(target, key, receiver) {
        console.log(`delete key ${key}`);
        return Reflect.deleteProperty(target, key, receiver);
      },
    });
    

    可以发现完美地实现这些功能。

    Proxy与Reflect的结合

    3. 代理数组

    我们在之前的文章 Vue 中对数组特殊的操作 中,讨论过 Vue 为什么没有使用Object.defineProperty来劫持数据,而是重写了 Array 原型链上的几个方法,通过这几个方法来实现 Vue 模板中数据的更新。

    但若 Proxy 的话,就可以直接代理数组:

    const arr = [1, 2, 3, 4];
    const arrProxy = new Proxy(arr, {
      get(target, key, receiver) {
        console.log('arrProxy.get', target, key);
        return Reflect.get(target, key, receiver);
      },
      set(target, key, value, receiver) {
        console.log('arrProxy.set', target, key, value);
        return Reflect.set(target, key, value, receiver);
      },
      deleteProperty(target, key) {
        console.log('arrProxy.deleteProperty', target, key);
        return Reflect.deleteProperty(target, key);
      },
    });
    

    虎鹤双形

    现在我们再来操作一下代理后的数组 arrProxy 看下:

    arrProxy[2] = 22; // arrProxy.set (4) [1, 2, 3, 4] 2 22
    arrProxy[3]; // arrProxy.get (4) [1, 2, 22, 4] 3
    delete arrProxy[2]; // arrProxy.deleteProperty (4) [1, 2, 22, 4] 2
    arrProxy.push(5); // push操作比较复杂,这里进行了多个get()和set()操作
    arrProxy.length; // arrProxy.get (5) [1, 2, empty, 4, 5] length
    

    可以看到无论获取、删除还是修改数据,都可以感知到。还有数组原型链上的一些方法,如:

    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()

    也都能通过 Proxy 中的代理方法劫持到。

    concat()方法比较特殊的是,他是一个赋值操作,并不改变原数组,因此在调用 concat()方法操作数组时,如果没有赋值操作,那么这里只有 get()拦截到。

    Proxy中的concat()操作

    4. 代理函数

    Proxy 中还有一个apply()方法,是表示自己作为函数调用时,被拦截的操作。

    const getSum = (...args) => {
      if (!args.every((item) => typeof item === 'number')) {
        throw new TypeError('参数应当均为number类型');
      }
      return args.reduce((sum, item) => sum + item, 0);
    };
    const fnProxy = new Proxy(getSum, {
      /**
       * @params {Fuction} target 代理的对象
       * @params {any} ctx 执行的上下文
       * @params {any} args 参数
       */
      apply(target, ctx, args) {
        console.log('ctx', ctx);
        console.log(`execute fn ${getSum.name}, args: ${args}`);
        return Reflect.apply(target, ctx, args);
      },
    });
    

    执行 fnProxy:

    // 10, ctx为undefined, log: execute fn getSum, args: 1,2,3,4
    fnProxy(1, 2, 3, 4);
    
    // ctx为undefined, Uncaught TypeError: 参数应当均为number类型
    fnProxy(1, 2, 3, '4');
    
    // 10, ctx为window, log: execute fn getSum, args: 1,2,3,4
    fnProxy.apply(window, [1, 2, 3, 4]);
    
    // 6, ctx为window, log: execute fn getSum, args: 1,2,3
    fnProxy.call(window, 1, 2, 3);
    
    // 6, ctx为person, log: execute fn getSum, args: 1,2,3
    fnProxy.apply(person, [1, 2, 3]);
    

    沉迷工作

    5. 一些简单的应用场景

    我们知道 Vue3 里已经用 Proxy 重写了响应式系统,mobx 也已经用了 Proxy 模式。在可见的未来,会有更多的 Proxy 的应用场景,我们这里也稍微讲解几个。

    5.1 统计函数被调用的上下文和次数

    这里我们用 Proxy 来代理函数,然后函数被调用的上下文和次数。

    const countExecute = (fn) => {
      let count = 0;
    
      return new Proxy(fn, {
        apply(target, ctx, args) {
          ++count;
          console.log('ctx上下文:', ctx);
          console.log(`${fn.name} 已被调用 ${count} 次`);
          return Reflect.apply(target, ctx, args);
        },
      });
    };
    

    现在我们来代理下刚才的getSum()方法:

    const getSum = (...args) => {
      if (!args.every((item) => typeof item === 'number')) {
        throw new TypeError('参数应当均为number类型');
      }
      return args.reduce((sum, item) => sum + item, 0);
    };
    
    const useSum = countExecute(getSum);
    
    useSum(1, 2, 3); // getSum 已被调用 1 次
    
    useSum.apply(window, [2, 3, 4]); // getSum 已被调用 2 次
    
    useSum.call(person, 3, 4, 5); // getSum 已被调用 3 次
    

    5.2 实现一个防抖功能

    基于上面统计函数调用次数的功能,也给我们实现一个函数的防抖功能添加了灵感。

    const throttleByProxy = (fn, rate) => {
      let lastTime = 0;
      return new Proxy(fn, {
        apply(target, ctx, args) {
          const now = Date.now();
          if (now - lastTime > rate) {
            lastTime = now;
            return Reflect.apply(target, ctx, args);
          }
        },
      });
    };
    
    const logTimeStamp = () => console.log(Date.now());
    window.addEventListener('scroll', throttleByProxy(logTimeStamp, 300));
    

    logTimeStamp()至少需要 300ms 才能执行一次。

    5.3 实现观察者模式

    我们在这里实现一个最简单类 mobx 观察者模式。

    const list = new Set();
    const observe = (fn) => list.add(fn);
    const observable = (obj) => {
      return new Proxy(obj, {
        set(target, key, value, receiver) {
          const result = Reflect.set(target, key, value, receiver);
          list.forEach((observer) => observer());
          return result;
        },
      });
    };
    const person = observable({ name: 'wenzi', age: 20 });
    const App = () => {
      console.log(`App -> name: ${person.name}, age: ${person.age}`);
    };
    observe(App);
    

    person就是使用 Proxy 创建出来的代理对象,每当 person 中的属性发生变化时,就会执行 App()函数。这样就实现了一个简单的响应式状态管理。

    昏昏欲睡

    6. Proxy 与 Object.defineProperty 的对比

    上面很多例子用Object.defineProperty也都是可以实现的。那么这两者都各有什么优缺点呢?

    6.1 Object.defineProperty 的优劣

    Object.defineProperty的兼容性可以说比 Proxy 要好很多,出特别低的 IE6,IE7 浏览器外,其他浏览器都有支持。

    但 Object.defineProperty 支持的方法很多,并且主要是基于属性进行拦截的。因此在 Vue2 中只能重写 Array 原型链上的方法,来操作数组。

    6.2 Proxy 的优劣

    Proxy与上面的正好相反,Proxy 是基于对象来进行代理的,因此可代理更多的类型,例如 Object, Array, Function 等;而且代理的方法也多了很多。

    劣势就是兼容性不太好,即使用 polyfill,也无法完美的实现。

    7. 总结

    Proxy 能实现的功能还有很多,后面我们也会继续进行探索,并且尽可能去了解下基于 Proxy 实现的类库,例如 mobx5 的源码和实现原理等。

    欢迎您关注我的公众号:

    前端小茶馆公众号

  • 相关阅读:
    CentOS7配置Tomcat8开机自动启动
    StackExchange.Redis 异步超时解决方案
    同一个tomcat部署多个项目导致启动失败
    吐血记录微信小程序授权获取Unionid及linux下使用bouncycastle解密用户数据 遇到的坑
    CentOS7设置ssh服务以及端口修改
    linux CentOS7安装与配置nginx1.18.0 并设置开机启动
    C语言函数指针用法
    Javascript优点和缺点
    VIM 单词大小写转换
    python base64编码实现
  • 原文地址:https://www.cnblogs.com/xumengxuan/p/14591071.html
Copyright © 2020-2023  润新知