The responsive principle of vue is realized through Proxy and Reflect

Posted by vronsky on Sat, 11 Dec 2021 15:17:14 +0100

vue3 implements the response expression through Proxy+Reflect, and vue2 implements it through defineProperty

Proxy

What is Proxy

Proxy is a class added in ES6 to represent proxy.
If we want to listen to the operation process of the object, we can first create a proxy object, and then all operations on the object are completed by the proxy object. The proxy object can listen to what operations we have performed on the original object.

How to use Proxy

Proxy is a class that creates an object through the new keyword, passes in the original object and the catcher that handles listening

const user = {
  name: 'alice'
}

const proxy = new Proxy(user, {})

console.log(proxy)
proxy.name = 'kiki'
console.log(proxy.name)
console.log(user.name)

The operation on the proxy will also act on the original object

What is a trap

A catcher is a method used to listen for operations on objects

const user = {
  name: 'alice',
  age: 18
}
const proxy = new Proxy(user, {
  get: function(target, key){
    console.log(`call ${key}Property`)
    return target[key]
  },
  set: function(target, key, value){
    console.log(`call ${key}Property setting operation`)
    target[key] = value
  }
})
proxy.name = 'kiki'
console.log(proxy.age)

The above get and set are commonly used in the catcher, which are respectively used for the "read" operation and "set" operation of object data

What traps are there

The operations and definitions of the corresponding catcher in the Proxy and ordinary objects are one-to-one corresponding

  1. handler.getPrototypeOf()
    Object. The catcher for the getprototypeof} method.
  2. handler.setPrototypeOf()
    Object. The catcher for the setprototypeof} method.
  3. handler.isExtensible()
    Object. The catcher for the isextensible() method.
  4. handler.preventExtensions()
    Object. The catcher for the preventextensions} method.
  5. handler. getOwnPropertyDescriptor() Object. The catcher for the getownpropertydescriptor} method.
  6. handler.defineProperty()
    Object. The catcher for the defineproperty method.
  7. handler.has()
    The snap for the in operator.
  8. handler.get()
    Property is the catcher for the read operation.
  9. handler.set()
    Property sets the snapper for the operation.
  10. handler.deleteProperty()
    The catcher for the delete operator.
  11. handler.ownKeys()
    Object.getOwnPropertyNames() method and {object The catcher for the getownpropertysymbols() method.
  12. handler.apply()
    Catcher for function call operations.
  13. handler.construct()
    The new operator's catcher.

These traps and the corresponding object operations are written in the following example

const user = {
  name: "alice",
  age: 18
};
const proxy = new Proxy(user, {
  get(target, key) {
    console.log("Yes get method");
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    console.log("Yes set method");
  },
  has(target, key) {
    return key in target;
  },
  deleteProperty(target, key){
    console.log('Yes delete New catcher')
    delete target[key]
  },
  ownKeys(target){
    console.log('ownKeys')
    return Object.keys(target)
  },
  defineProperty(target, key, value){
    console.log('defineProperty', target, key, value)
    return true
  },
  getOwnPropertyDescriptor(target, key){
    return Object.getOwnPropertyDescriptor(target, key)
  },
  preventExtensions(target){
    console.log('preventExtensions')
    return Object.preventExtensions(target)
  },
  isExtensible(target){
    return Object.isExtensible(target)
  },
  getPrototypeOf(target){
    return Object.getPrototypeOf(target)
  },
  setPrototypeOf(target, prototype){
    console.log('setPrototypeOf', target, prototype)
    return false
  }
});

console.log(proxy.name);
proxy.name = "kiki";
console.log("name" in proxy);
delete proxy.age
console.log(Object.keys(proxy))
Object.defineProperty(proxy, 'name', {
  value: 'alice'
})
console.log(Object.getOwnPropertyDescriptor(proxy, 'name'))
console.log(Object.preventExtensions(proxy))
console.log(Object.isExtensible(proxy))
console.log(Object.getPrototypeOf(proxy))
Reflect.setPrototypeOf(proxy, {})

Monitor the modification, query, deletion and other operations of objects through the catcher

In the above captors, only apply and constructor belong to function objects

function foo(){}

const proxy = new Proxy(foo, {
  apply: function(target, thisArg, args){
    console.log('implement proxy of apply method', target, thisArg, args)
    return target.apply(thisArg, args)
  },
  construct: function(target, argArray){
    console.log('implement proxy of construct method',target, argArray)
    return new target()
  }
})

proxy.apply({}, [1,2])
new proxy('alice', 18)

The apply method executes the apply catcher, and the new operation executes the constructor catcher

Reflect

What is Reflect

Reflection is also a new API in ES6, which represents reflection. It is an Object that provides many methods to manipulate objects, similar to the methods in Object, such as Reflect Getprototypeof and Object getPrototypeOf.

In the early days, the methods of operating objects were defined on Object, but it was not appropriate to directly put Object as a constructor on it, so a new Reflect Object was added to operate uniformly, and operators such as in and delete in the Object were converted

What are the methods in Reflect

The methods in Reflect correspond to those in Proxy one by one

  1. Reflect.apply(target, thisArgument, argumentsList)
    When calling a function, you can pass in an array as the call parameter. And function prototype. The function of apply () is similar.
  2. Reflect.construct(target, argumentsList[, newTarget])
    The {new} operation on the constructor is equivalent to executing} new target(...args).
  3. Reflect.defineProperty(target, propertyKey, attributes) and {object Defineproperty() is similar. If the setting is successful, it will return {true
  4. Reflect.deleteProperty(target, propertyKey)
    The delete operator as a function is equivalent to executing delete target[name].
  5. Reflect.get(target, propertyKey[, receiver])
    Gets the value of an attribute on the object, similar to target[name].
  6. Reflect.getOwnPropertyDescriptor(target, propertyKey)
    Similar to object getOwnPropertyDescriptor(). If the property exists in the object, the corresponding property descriptor is returned; otherwise, it returns {undefined
  7. Reflect.getPrototypeOf(target)
    Similar to object getPrototypeOf().
  8. Reflect.has(target, propertyKey)
    The function of determining whether an object has a property is exactly the same as that of the {in} operator.
  9. Reflect.isExtensible(target)
    Similar to object isExtensible().
  10. Reflect.ownKeys(target)
    Returns an array containing all its own properties (excluding inherited properties). (similar to {Object.keys(), but not affected by enumerable)
  11. Reflect.preventExtensions(target)
    Similar to object preventExtensions(). Returns a Boolean.
  12. Reflect.set(target, propertyKey, value[, receiver])
    A function that assigns a value to a property. Returns a Boolean. If the update is successful, it returns true.
  13. Reflect.setPrototypeOf(target, prototype)
    Function to prototype an object Returns a Boolean. If the update is successful, it returns true.

Change all the object operations previously set up through Proxy to Reflect

const user = {
  name: "alice",
  age: 18
};
const proxy = new Proxy(user, {
  get(target, key) {
    console.log("Yes get method");
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    Reflect.set(target, key, value)
    console.log("Yes set method");
  },
  has(target, key) {
    return Reflect.has(target, key)
  },
  deleteProperty(target, key){
    Reflect.deleteProperty(target, key)
  },
  ownKeys(target){
    console.log('ownKeys')
    return Reflect.ownKeys(target)
  },
  defineProperty(target, key, value){
    console.log('defineProperty', target, key, value)
    return true
  },
  getOwnPropertyDescriptor(target, key){
    return Reflect.getOwnPropertyDescriptor(target, key)
  },
  preventExtensions(target){
    console.log('preventExtensions')
    return Reflect.preventExtensions(target)
  },
  isExtensible(target){
    return Reflect.isExtensible(target)
  },
  getPrototypeOf(target){
    return Reflect.getPrototypeOf(target)
  },
  setPrototypeOf(target, prototype){
    console.log('setPrototypeOf', target, prototype)
    return false
  }
});
console.log(proxy.name);
proxy.name = "kiki";
console.log(Reflect.has(proxy, 'name'));
delete proxy.age
console.log(Reflect.ownKeys(proxy))
Reflect.defineProperty(proxy, 'name', {
  value: 'alice'
})
console.log(Reflect.getOwnPropertyDescriptor(proxy, 'name'))
console.log(Reflect.preventExtensions(proxy))
console.log(Reflect.isExtensible(proxy))
console.log(Reflect.getPrototypeOf(proxy))
Reflect.setPrototypeOf(proxy, {})

The effect is completely consistent

receiver of Reflect

During the get/set catcher operation in Reflect, another input parameter is receiver, which refers to the proxy object, which is used to change the direction of this

const user = {
  _name: 'alice',
  get name(){
    return this._name
  },
  set name(value){
    this._name = value
  }
}
const proxy = new Proxy(user, {
  get: function(target, key, receiver){
    console.log('get operation', key)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, receiver){
    console.log('set operation', key)
    return Reflect.set(target, key, receiver)
  },
})
console.log(proxy.name)
proxy.name = 'kiki'

(1) If there is no receiver, when modifying the name attribute, objProxy first performs the get operation when the key is name
(2) Then proxy to the get method in obj to read this_ name attribute. At this time, this is obj, which will be modified directly
obj._name, no more objProxy
(3) After the receiver is added, execute the get method of obj to read this_ name attribute. At this time, this is a proxy
Object, so it will be in the catcher of get again

The same is true for set operations

constructor in Reflect

Used to change the direction of this

function Person(){
}
function Student(name, age){
  this.name = name;
  this.age = age
}
const student = Reflect.construct(Student, ['aclie', 18], Person)

console.log(student)
console.log(student.__proto__ === Person.prototype)

Although the student object created at this time has the properties and methods of student, its this point to Person

Response of vue

Step by step to achieve responsiveness through the following steps

1. Define an array to collect all dependencies

  • The global variable reactiveFns is defined to hold all functions
  • Define the method watchFn, the input parameter is a function, and the code body is to save the input parameter into the global variable
  • Modify the value of the object, traverse the global variables, and execute each function
let user = {
  name: "alice",
};
let reactiveFns = [];
function watchFn(fn) {
  reactiveFns.push(fn);
}
watchFn(function () {
  console.log("Ha ha ha");
});
watchFn(function () {
  console.log("hello world");
});
function foo(){
  console.log('Ordinary function')
}
user.name = "kiki";
reactiveFns.forEach((fn) => {
  fn();
});

At this time, all the functions to be executed are collected into the array through the responsive function watchFn. Then, when the value of the variable changes, manually traverse and execute all the functions

2. Collect the encapsulation of dependent classes

There is only one array above to collect the execution functions of objects. In fact, more than one object needs to listen to the operation status. You can use classes if you need to listen to multiple objects.

  • Define the append class, provide addappend method for each instance object to add binding methods, and notify method to execute all methods added on the reactiveFns attribute of the instance
  • Encapsulate the responsive function watchFn, which calls the appappend method of the instance object
  • Modify the value of the object and call the notify method of the instance object
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
watchFn(function(){
  console.log('Lala Lala')
})
watchFn(function(){
  console.log('hello hello')
})
function foo(){
  console.log('foo')
}
user.name = 'kiki'
depend.notify()

The collection operation and the method of executing the function in turn are defined in the class

3. Automatically monitor the changes of objects

The above is still our method of manually calling the execution function, and the following is automatic listening

  • On the basis of collecting dependencies through classes, add Proxy to define objects, and Reflect to execute methods of objects
  • Execute the notify method of the instance object depend ent in the set method
  • Modify the value of the proxy property
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
const proxy = new Proxy(user, {
  get: function (target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    depend.notify();
  },
});
watchFn(function () {
  console.log("name Change the function performed");
});
watchFn(function () {
  console.log("age Change the function performed");
});
function foo() {
  console.log("foo");
}
proxy.name = "kiki";
proxy.age = 20;

The problem at this time is that if any attribute of the object is modified, all functions will be called, and the attribute and corresponding function of the object are not saved according to the one-to-one correspondence

4. Collection dependent management

  • Define weakMap to manage objects, [object name, map], define map to save [object attribute name, instance object depend ent]
  • Define the method getappend to obtain the dependent instance object. First obtain the map from the weakMap. If there is no new one, then obtain the dependent object from the map. If there is no new one, then obtain the dependent object from the map
  • Get the depend ent object in the set method and call the notify method
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
const weakMap = new WeakMap()
function getDepend(obj, key){
  let map = weakMap.get(obj)
  if(!map){
    map = new Map()
    weakMap.set(obj, map)
  }
  let depend = map.get(key)
  if(!depend){
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
const proxy = new Proxy(user, {
  get: function (target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    const depend = getDepend(target, key)
    depend.notify();
  },
});
watchFn(function () {
  console.log("name Change the function performed");
});
watchFn(function () {
  console.log("age Change the function performed");
});
function foo() {
  console.log("foo");
}
proxy.name = "kiki";
proxy.age = 20;

At this time, the depend ent corresponding to the proxy has no value, so there is no printed data at this time

5. Manage collected dependencies correctly

  • The global definition variable activeReactiveFn points to the method passed in by watchFn, executes the passed in method, and then points to null
  • When watchFn is passed in, it will be executed once to collect dependencies in the get method of the code object
  • The addappend method used in the append class directly uses the global activeReactiveFn without passing parameters
  • In the get method, get the dependent through getDepend, and use the addDepend method to collect the dependency
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
let activeReactiveFn = null;
function watchFn(fn) {
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}
const weakMap = new WeakMap();
function getDepend(obj, key) {
  let map = weakMap.get(obj);
  if (!map) {
    map = new Map();
    weakMap.set(obj, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}
const proxy = new Proxy(user, {
  get: function (target, key, receiver) {
    const depend = getDepend(target, key)
    depend.addDepend(activeReactiveFn)
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    const depend = getDepend(target, key);
    depend.notify();
  },
});
watchFn(function () {
  console.log(proxy.name, "name Change the function performed");
  console.log(proxy.name, "name Change the function performed again");
});
watchFn(function () {
  console.log(proxy.age, "age Change the function performed");
});
function foo() {
  console.log("foo");
}
proxy.name = "kiki";

At this time, the corresponding function can be executed according to the change of attribute value, but the same function will be executed twice

6. Reconstruction

  • When the property of the two proxy is invoked in the function, the same function is added to the array two times, so the reactiveFn data structure is changed from array to set.
  • Define the reactive function to collect and process the objects that need proxy and response
let activeReactiveFn = null;
class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  addDepend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn);
    }
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
function watchFn(fn) {
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}
const weakMap = new WeakMap();
function getDepend(obj, key) {
  let map = weakMap.get(obj);
  if (!map) {
    map = new Map();
    weakMap.set(obj, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}
function reactive(obj){
  return new Proxy(obj, {
    get: function (target, key, receiver) {
      const depend = getDepend(target, key);
      depend.addDepend();
      return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      const depend = getDepend(target, key);
      depend.notify();
    },
  });
}
const user = reactive({
  name: "alice",
  age: 18,
})
const info = reactive({
  message: 'hello'
})
watchFn(function () {
  console.log(user.name, "name Change the function performed");
  console.log(user.name, "name Change the function performed again");
});
watchFn(function () {
  console.log(user.age, "age Change the function performed");
});
watchFn(function(){
  console.log(info.message, "message Something has changed")
})
function foo() {
  console.log("foo");
}
user.name = "kiki";
user.age = 20;
info.message = 'ooo'

At this point, the response of vue3 has been realized~

Implementation principle of vue2

The implementation of vue2 is to replace Proxy and Reflect with Object Some methods of defineproperty and Object itself

let activeReactiveFn = null;
class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  addDepend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn);
    }
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
function watchFn(fn) {
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}
const weakMap = new WeakMap();
function getDepend(obj, key) {
  let map = weakMap.get(obj);
  if (!map) {
    map = new Map();
    weakMap.set(obj, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}
function reactive(obj){
  Object.keys(obj).forEach(key=>{
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function () {
        const depend = getDepend(obj, key);
        depend.addDepend();
        return value
      },
      set: function (newValue) {
        value = newValue
        const depend = getDepend(obj, key);
        depend.notify();
      },
    })
  })
  return obj
}
const user = reactive({
  name: "alice",
  age: 18,
})
const info = reactive({
  message: 'hello'
})
watchFn(function () {
  console.log(user.name, "name Change the function performed");
  console.log(user.name, "name Change the function performed again");
});
watchFn(function () {
  console.log(user.age, "age Change the function performed");
});
watchFn(function(){
  console.log(info.message, "message Something has changed")
})
function foo() {
  console.log("foo");
}
user.name = "kiki";
user.age = 20;
info.message = 'ooo'

It is consistent with the effect achieved above

The above is the responsive principle of realizing vue through Proxy and Reflect. There are still many places for developers to master about js advanced. You can see other blog posts I wrote and keep updating~

Topics: Javascript Front-end ECMAScript Vue.js