Basic implementation

Background: In the current project, the Proxy Api was used extensively, but the product suddenly suggested that the project could not be opened by Internet Explorer, so it was forced to open….

At present, the implementation is Proxy set,get,construct interception, the core principle is still familiar with Object. DefineProperty, not to talk about the code

const ISALIVE = Symbol('isAlive');
function Proxy(pasproxy = {}, handler = {}) {
   // Type error handling
  if(! (pasproxyinstanceof Object) | |! (handler? .constructor ===Object)) {
    throw new Error(`Cannot create proxy with a non-object as target or handler`)}//@ first define the default Proxy method. In Proxy, handler does not report errors, but for convenience I give it a default value, which takes the properties of the Proxy object
  const _handler = {
    get(obj, key) {
      return obj[key];
    },
    set(obj, key, value, proxy) {
      // console.log('proxy',obj,key,value,)
      return(obj[key] = value); }};Object.assign(_handler, handler);
  
 
  const targetIsArray = pasproxy instanceof Array
  
  // @mirror is a mirror property. It acts as the middle layer between the proxy-object and the proxy relationship. Calling defineProperty directly on the proxy-object will pollute the data source.
  const mirror = pasproxy && targetIsArray ? [] : {};

  Object.keys(pasproxy).some((k, index) = > {
   // Give the current attribute a special identifier
    mirror[k] = ISALIVE;
    
    Object.defineProperty(mirror, k, {
      get() {
        return _handler.get(pasproxy, k);
      },
      set(value) {
        return_handler.set(pasproxy, k, value, pasproxy); }}); }); mirror['[[Target]]'] = pasproxy
  Object.defineProperty(mirror, '[[Target]]', { enumerable: false })
  return mirror
}
const p = new Proxy({name:'juejin'}, {get(obj,key){return obj[key] + '! '}})
p.name // juejin ! 

Copy the code

Mirror acts as the middle layer. When a set or GET method responds, it always returns the key of the propped object, so properties on the prototype can also be forwarded to.

Mirror [k] = ISALIVE: mirror[k] = ISALIVE: mirror[k] = ISALIVE: mirror[k] = ISALIVE: mirror[k] = ISALIVE

Since mirror is an intermediate layer and its properties are only a bridge to the proxy object, it cannot accurately reflect the data.

Mirror [‘[[Target]]’] is used to view the property value of the current proxied object for debugging

The biggest problem with the current version is that:

{a:1} {b: 2} {a:1} {b: 2} {a:1} {b: 2} {a:1} {b: 2} {a:1} {b: 2} {a:1} {b: 2} {b: 2}

2. Methods that instantiate objects such as Date,Array, slice, date.now cannot be intercepted (e.g.,Array, slice, date.now)

Pain points of native Proxy

For the first point, there is no good method for dynamic interception attribute except dirty check at present, and there is no way to ensure consistency, but there is no need to tangle about this. The interception method of native Proxy also has some burdiness, such as:

You see you only want to intercept slice, but attributes like length and constructor that you don’t need to respond to also respond. This puts an extra burden on the program.

In fact, when we use Proxy, we should predict the object or attribute to be intercepted in advance, because you can only do the corresponding operation on it if you know what you want to intercept. Therefore, for attribute interception, we can declare in advance or post register. But pre-declaration is not very elegant in terms of data structure creep, and it’s pretty bad for key-value operations on arrays

Ok, so let’s implement post-registration:

Solve the problem of dynamic properties by implant method

Add a implant method to the Proxy column:

const ISALIVE = Symbol('isAlive');
function Proxy(pasproxy = {}, handler = {}) {

  if(! (pasproxyinstanceof Object) | |! (handler? .constructor ===Object)) {
    throw new Error(`Cannot create proxy with a non-object as target or handler`)}const _handler = {
    get(obj, key) {
      return obj[key];
    },
    set(obj, key, value, proxy) {
      // console.log('proxy',obj,key,value,)
      return (obj[key] = value);
    },
    construct(target, args) {
      return newtarget(... args); },max: 100.// Prevent the array from becoming too large
  };


  Object.assign(_handler, handler);
  
  const implant = (k, initValue) = > {
    // Prevent adding invalid fields like undefinde
    if (Type.isEmpty(k)) return;
    // Do not re-register already registered properties
    if (Key.get(mirror, k) === ISALIVE) return;
    
    // Prevent late registration when attributes are deleted
    if (mirror[k] === undefined && Key.get(mirror, k) === ISALIVE) {
      Key.del(mirror, k)
    }
    
    mirror[k] === undefined ? mirror[k] = ISALIVE : null;
    pasproxy[k] === undefined ? pasproxy[k] = initValue : null;
    
    // Anti-duplicate label
    Key.set(mirror, k, ISALIVE)
    
    Object.defineProperty(mirror, k, {
      get() {
        return _handler.get(pasproxy, k);
      },
      set(value) {
        return_handler.set(pasproxy, k, value, pasproxy); }}); };const targetIsArray = pasproxy instanceof Array
  const mirror = pasproxy && targetIsArray ? [] : {};

  mirror.implant = implant;
  
  // Make the property untraversable to prevent unexpected results during iteration
  Object.defineProperty(mirror, 'implant', { enumerable: false })

  //
  Object.keys(pasproxy).some((k, index) = > {
    mirror[k] = ISALIVE;
    Object.defineProperty(mirror, k, {
      get() {
        return _handler.get(pasproxy, k);
      },
      set(value) {
        return_handler.set(pasproxy, k, value, pasproxy); }});// Prevent unnecessary listening in case the array data is too large
    if (targetIsArray && index >= _handler.max) return true 
  });

  mirror['[[Target]]'] = pasproxy
  Object.defineProperty(mirror, '[[Target]]', { enumerable: false })
  return mirror
}
const p = new Proxy({name:'juejin'}, {get(obj,key){return obj[key] + '! '}})
p.implant('loc'.'China')
p.loc / / China!
Copy the code

Let’s start with ISALIVE, which is essentially a placeholder to determine if the current property is registered and already exists, since objects may be subject to delete obj[key]

Such operation, so that the bridge of information communication is broken, but the attributes are faced with the possibility of being monitored again, for example, the table can be edited again, and unnecessary keys need to be deleted when submitting data.

After data is sent back, the attributes of the deleted key still need to be listened again.

Interception of the real column method

The _handler. Max attribute, although necessary for operations such as: arr[N], is in most cases not all keys should be monitored in real time. If there is a monitoring requirement such as 1K and 1W, the Max value can be manually changed to achieve this purpose

And then the problem of dynamic properties is also somewhat solved, so how to handle the methods on the object of the column? Such as intercepting an array or a method on a date, or intercepting

Log method on class A

class A{
  constructor(){}
  log(){}}Copy the code

Now the mainstream approach is to delegate a method directly, for example

arr.slice=(. arg) = >{ Arrar.prototype.slice.call .... .return. }Copy the code

Now we can implant directly!

const p = new Proxy([] and {get(obj,key){
 if(key==='slice') {return () = >[1.2.3]}return obj[key]
}})

p.implant('slice')
p.slice() / / [1, 2, 3]
Copy the code

The code is much simpler!

But what if registering slice every time a new Proxy is redundant?

The way I came up with is to add a field table for the data, such as adding a set of inherent property fields for the commonly used Array data type, and then iterating automatically with a type-checking function each time the Array type is proxy

Define the registry and Proxy type checking functions


/ / the registry
const typeGroup = [
  // Prespecify all methods on the array
  {
    type: Array.fields: `concat,constructor,copyWithin,entries,every,fill,filter,find,findIndex,flat,flatMap,forEach,includes,indexOf,join,keys, lastIndexOf,map,pop,push,reduce,reduceRight,reverse,shift,slice,some,sort,splice,toLocaleString,toString,unshift,values`.split(', ')
  },


]

typeGroup.set = (val) = > {
  if(! val.constructor ===Object) throw new Error('New registry type error')
  typeGroup.some(member= > {
    // Same type merge
    if (member.type === val.type) {
      member.fields = [...member.fields, ...val.fields]
      return true
    }
  }) || typeGroup.push(val)

}

//typecheck 
function ProxyTypeCheck(target, handler) {
  const proxy = Proxy(target, handler)
   
  typeGroup.some(({ type, fields }) = > {
    if (target instanceof type) {
      fields.forEach(k= > proxy.implant(k))
      return true}})return proxy
}

ProxyTypeCheck.type = (type, arr) = > {
  const _arr = Array.isArray(arr) ? arr : [arr]
  typeGroup.set({ type, fields: _arr })

}

module.exports = ProxyTypeCheck;

Copy the code

Let’s try importing it

import Proxy form 'Proxy'

Proxy.type(A,'log')
Proxy.type(Date['now'.'parse'])
const p = new Proxy([] and {get(obj,key){
 if(key==='slice') {return () = >[1.2.3]}return obj[key]
}})

p.slice() / / [1, 2, 3]

const p2= new Proxy(new A(),{get(obj,key){
 if(key==='log') {return (str) = >{ console.log(str)}
 }
 return obj[key]
}})

p2.log('hello word') // helloword 


Copy the code

Well, this looks like an end, but in the native Proxy Api it is possible to intercept constructors executed by the new operator

Intercept the construct constructor

function monster1(disposition) {
  this.disposition = disposition;
}

const handler1 = {
  construct(target, args) {
    console.log('monster1 constructor called');
    // expected output: "monster1 constructor called"

    return new target(...args);
  }
};

const proxy1 = new Proxy(monster1, handler1);

console.log(new proxy1('fierce').disposition);
// expected output: "fierce"
Copy the code

It’s spitting blood. Let’s try again and see if this damn thing can intercept it

function Proxy(pasproxy = {}, handler = {}) {

  if(! (pasproxyinstanceof Object) | |! (handler.constructor ===Object)) {
    throw new Error(`Cannot create proxy with a non-object as target or handler`)}const _handler = {
    get(obj, key) {
      return obj[key];
    },
    set(obj, key, value, proxy) {
      // console.log('proxy',obj,key,value,)
      return (obj[key] = value);
    },
    construct(target, args) {
      return newtarget(... args); },max: 100};Object.assign(_handler, handler);

  const implant = (k, initValue) = > {
    if (Type.isEmpty(k)) return;
    if (Key.get(mirror, k) === ISALIVE) return;

    if (mirror[k] === undefined && Key.get(mirror, k) === ISALIVE) {
      Key.del(mirror, k)
    }

    mirror[k] === undefined ? mirror[k] = ISALIVE : null;
    pasproxy[k] === undefined ? pasproxy[k] = initValue : null;
    Key.set(mirror, k, ISALIVE)
    // Object.defineProperty(mirror, k, { enumerable: false })
    Object.defineProperty(mirror, k, {
      get() {
        return _handler.get(pasproxy, k);
      },
      set(value) {
        return_handler.set(pasproxy, k, value, pasproxy); }}); };// Intercept the new operator
  const ProxyTarget = function (. arg) {
    return _handler.construct(pasproxy, arg)
  }

  const targetIsFunction = typeof pasproxy === 'function'
  const targetIsArray = pasproxy instanceof Array
  const mirror = pasproxy && targetIsArray ? [] : (targetIsFunction ? ProxyTarget : {});

  mirror.implant = implant;

  Object.defineProperty(mirror, 'implant', { enumerable: false })

  //
  Object.keys(pasproxy).some((k, index) = > {
    mirror[k] = ISALIVE;
    Object.defineProperty(mirror, k, {
      get() {
        return _handler.get(pasproxy, k);
      },
      set(value) {
        return_handler.set(pasproxy, k, value, pasproxy); }});if (targetIsArray && index >= _handler.max) return true
  });

  mirror['[[Target]]'] = pasproxy
  Object.defineProperty(mirror, '[[Target]]', { enumerable: false })
  return mirror
}
Copy the code

Not a big change. Let’s try it

class A {
  constructor(){}log() {
    console.log('log')}}const p = new Proxy(A, {
  construct(target, args) {
    console.log('monster1 constructor called');
    return new target(...args);
  }
})

new p().log()
Copy the code

All right! You’re done

Outstanding questions

Although the most commonly used Proxy apis are GET and SET, there are still a lot of things that cannot be made up for by looking through Proxy documentation

Such as:

Handler.setprototypeof () Prototype chain operation interception

Handler.deleteproperty () intercepts the delete operator

Handler. Has () in operator intercepts

Although it cannot achieve 100% implementation of Proxy, the author has no major problems in the actual use of the encapsulation of the two basic APIS. Of course, the leaders have better methods to discuss more and give more advice.

The final summary

Efforts to be compatible with proxies

1. Forge the Proxy Api by encapsulating Object.defineProperty

2. Solve the problem of dynamic properties through implant+ property registry

3. Reduce the redundancy of implant through type checking function

4. Use the Max attribute to solve the pain point of oversized arrays