0

带你深入领略 Proxy 的世界

 3 years ago
source link: https://www.xiabingbao.com/post/js/js-proxy.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Proxy是在ES2015中添加的语法,考虑到兼容性的问题,我们实际中用的并不多,但随着浏览器的发展,对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,也无法完美的实现。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK