Proxy is a syntax added to the es2015 standard specification, and it is likely that you have only heard of it but not used it - after all, Proxy features cannot be used easily given the compatibility issues.

But now, with each browser’s update iteration, Proxy’s support is getting higher and higher.

js Proxy

Vue3 has replaced Object.defineProperty with Proxy for responsiveness, and mobx has been using Proxy for proxying since version 5.x.

1. The basic structure of Proxy

The basic usage of Proxy.

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

For example, if we want to proxy an object, we can set the get and set methods to proxy the operations of getting and setting data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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;
  },
});

The Proxy is just a proxy, the personProxy has all the properties and methods of the person. When we get and set the name through personProxy, there will be a corresponding log output.

1
2
3
4
5
personProxy.name; // "wenzi"
// log: get value by name

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

And when the data is set by personProxy, the data in the original structure of the proxy will also change. If we print the person, we can see that the value of the field name has also changed to hello .

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

The 2nd parameter of the Proxy, handler, has more rich methods besides setting get and set methods.

  • get(target, propKey, receiver): Read the properties of the interceptor, such as proxy.foo and proxy[‘foo’].
  • set(target, propKey, value, receiver):` Set the properties of the interceptor object, such as proxy.foo = v or proxy[‘foo’] = v, returning a boolean value.
  • has(target, propKey): Intercept the operation of propKey in proxy, returns a boolean value.
  • deleteProperty(target, propKey): intercepts the delete proxy[propKey] operation, returns a Boolean.
  • ownKeys(target): intercepts Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy), for. .in loop, returning an array. This method returns the property names of all the target object’s own properties, while Object.keys() returns only the target object’s own traversable properties.
  • getOwnPropertyDescriptor(target, propKey): Intercepts Object.getOwnPropertyDescriptor(proxy, propKey) and returns the description object of the property.
  • defineProperty(target, propKey, propDesc): intercepts Object.defineProperty(proxy, propKey, propDesc), Object.defineProperties(proxy, propDescs). Returns a boolean value.
  • preventExtensions(target): intercept Object.preventExtensions(proxy), returns a boolean.
  • getPrototypeOf(target): intercept Object.getPrototypeOf(proxy), returns an object.
  • isExtensible(target): intercept Object.isExtensible(proxy), returns a boolean.
  • setPrototypeOf(target, proto): intercept Object.setPrototypeOf(proxy, proto), returns a boolean. If the target object is a function, then there are two additional operations that can be intercepted.
  • apply(target, object, args): intercepts operations where the Proxy instance is called as a function, such as proxy(.. . args), proxy.call(object, . . args), proxy.apply(…) .
  • construct(target, args): intercepts the operation of a Proxy instance called as a constructor, such as new proxy(… . args).

For example, if we delete one of the elements by delete, we can intercept this operation by using the deleteProperty() method. We add a deleteProperty.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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];
  },
});

When the delete operation is performed.

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

2. Proxy and Reflect

Proxy and Reflect are inseparable. All the methods and usage in Reflect are exactly the same as Proxy.

For example, the get(), set() and deleteProperty() methods in the Proxy above operate directly on the original proxy object, but here we use Reflect instead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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);
  },
});

Perfect implementation of these functions can be found.

sobyte

3. Proxy arrays

Instead of using Object.defineProperty to hijack data, Vue overrides a few methods in the Array prototype chain to update data in the Vue template.

But with Proxy, you can proxy the array directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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);
  },
});

Now let’s manipulate the array arrProxy again after the proxy and see.

1
2
3
4
5
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

You can see that you can sense whether you are getting, deleting or modifying data. There are also some methods on the array prototype chain, such as

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

It can also be hijacked by the proxy method in the Proxy.

The special thing about the concat() method is that it is an assignment operation and does not change the original array, so when the concat() method is called to manipulate the array, if there is no assignment operation, then only get() will intercept it.

sobyte

4. Proxy functions

There is also an apply() method in Proxy, which is to indicate the operation that is intercepted when called as a function by itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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);
  },
});

Execute fnProxy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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. Some simple usage scenarios

We know that Vue3 has already rewritten the responsive system with Proxy, and mobx has also used Proxy pattern. In the foreseeable future, there will be more Proxy scenarios, so we will explain a few of them here.

5.1 Counting the context and number of times a function has been called

Here we use Proxy to proxy the function and then the context and number of times the function was called.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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);
    },
  });
};

Now let’s proxy the getSum() method we just used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 Implementing an anti-shake function

Based on the above function counting the number of function calls, it also adds inspiration to implement an anti-shake function for our functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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() takes at least 300ms to execute once.

5.3 Implementing the Observer pattern

Here we implement the simplest class of mobx observer pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 is the proxy object created using the Proxy, and the App() function is executed whenever the properties in person change. This results in a simple responsive state management.

6. Proxy versus Object.defineProperty

Many of the above examples are also possible with Object.defineProperty. So what are the advantages and disadvantages of each of these two?

6.1 Advantages and disadvantages of Object.defineProperty

The compatibility of Object.defineProperty is arguably much better than Proxy, with support in all browsers except IE6 and IE7, which are particularly low.

However, Object.defineProperty supports many methods and is mainly property-based for interception. So in Vue2 you can only override methods in the Array prototype chain to manipulate arrays.

6.2 Advantages and disadvantages of Proxy

Proxy is the opposite of the above. Proxy is object-based, so it can proxy more types, such as Object, Array, Function, etc.; and there are many more methods to proxy.

The disadvantage is that it is not very compatible, even if you use polyfill, it is not perfectly implemented.

7. Summary

There are many more features that Proxy can achieve, and we will continue to explore them and learn as much as possible about Proxy-based libraries, such as the source code and implementation principles of mobx5.