Interviewer: Can you write the responsive principles of the Vue2 by hand? We have seen the reactive principle in Vue2 and implemented its core logic. However, Vue2’s responsive principle has some disadvantages:

  • The default is recursive and consuming
  • Array responsivity requires additional implementation
  • Add/Delete properties Properties cannot be listened on
  • Map, Set, and Class cannot respond, and syntax modification is restricted

Vue3 uses ES6’s Proxy feature to solve these problems. In this article, I will take you through the responsive principle of Vue3 and finally implement its core logic through Proxy.

Before we start our analysis, what is a Proxy?

What is a Proxy?

In ES6, we saw a refreshing property called Proxy. Let’s take a look at the concept:

By calling new Proxy(), you create a Proxy to replace another object (called a target) that virtualizes the target object so that the Proxy and the target object can be treated ostensibly as the same object. Proxies allow you to intercept low-level operations on target objects, which is the internal capability of the JS engine.

A Proxy is a feature that allows you to manipulate objects at will. When we use Proxy to Proxy an object, we will get an object almost exactly the same as the proxied object, and this object can be completely monitored.

What do you mean by complete monitoring? What the Proxy provides is interception of the underlying operations. We used Object.defineProperty when we implemented listening on objects. This is actually a high-level operation provided by JS, which is exposed through low-level encapsulation. The power of proxies is that we can directly intercept the underlying operations on Proxy objects. In this way, we are listening to an object from the bottom of the operation.

So what are the advantages of Proxy over Object.defineProperty?

The advantages of Proxy

  • ProxyYou can listen directly on objects rather than properties;
  • ProxyYou can listen for array changes directly;
  • ProxyThere are up to 13 interception methods, not limited toapply,ownKeys,deleteProperty,hasAnd so on isObject.definePropertyNot having;
  • ProxyIt returns a new object, and we can just manipulate the new object for our purposes, whileObject.definePropertyCan only be modified directly by traversing object properties;
  • ProxyAs the new standard will be subject to browser vendors’ focus on continuous performance optimization, also known as the new standard performance dividend.

Now that I have a general understanding of Proxy, LET me analyze the responsivity principle of Vue3

Response principle

Here is a flow chart of Vue3 responsiveness that I compiled earlier:

Let’s go through the process:

State = reactive(Target)

2. Effect is used to declare a function cb that depends on reactive data (such as the render function) and execute the cb function, which triggers the getter for reactive data

3. Collect track dependencies in the getter of responsive data: store the mapping between responsive data and update function CB and store it in targetMap

4. When the responsive data is changed, trigger will be triggered to find the associated CB according to targetMap and execute it

TargetMap {target: {key: [fn1,fn2]}}

Handwritten implementation

Take a look at the core functions we are implementing:

  • reactive: reactive core method, used to establish data responsiveness
  • effect: declares the response function cb, saves the callback function for use, and immediately executes a callback function to trigger the getter for some of its response data
  • track: depends on collecting and storing the mapping between responsive data and update function CB
  • trigger: Trigger update: Perform cb based on the mapping

Establish the data response formula (reactiveFunction)

// Determine if it is an object
function isObject(val) {
  return typeof val === "object"&& val ! = =null;
}
function hasOwn(target, key) {
  return target.hasOwnProperty[key];
}
// WeakMap: Weakreference mapping table
// Original object: proxied object
let toProxy = new WeakMap(a);// Proxied object: original object
let toRaw = new WeakMap(a);// Reactive core methods
function reactive(target) {
  // Create a responsive object
  return createReactiveObject(target);
}
function createReactiveObject(target) {
  // If the object is not currently an object, just return it
  if(! isObject(target)) {return target;
  }
  // If already proxied, the proxied result is returned directly
  let proxy = toProxy.get(target);
  if (proxy) {
    return proxy;
  }
  // Prevent proxied objects from being proxied again
  if (toRaw.has(target)) {
    return target;
  }
  let baseHandler = {
    get(target, key, receiver) {
      // Reflect is a built-in object that provides methods to intercept JavaScript operations. These methods are the same as the proxy Handlers method.
      let res = Reflect.get(target, key, receiver);
      // Collect dependencies/subscriptions to map current keys to effects
      track(target, key);
      // If the value of get is an object, then it is recursive (this is an optimization compared to the default recursion in Vue2).
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      // We need to distinguish between new attributes and modified attributes
      let hasKey = hasOwn(target, key);
      let oldVal = target[key];
      let res = Reflect.set(target, key, value, receiver);
      if(! hasKey) {console.log("New Properties");
        trigger(target, "add", key);
      } else if(oldVal ! == value) {console.log("Modify properties");
        trigger(target, "set", key);
      }
      return res;
    },
    deleteProperty(target, key) {
      let res = Reflect.deleteProperty(target, key);
      returnres; }};let observed = new Proxy(target, baseHandler);
  toProxy.set(target, observed);
  toRaw.set(observed, target);
  return observed;
}
Copy the code

Depend on the collection

Update key (); update key ();

let obj = reactive({ name: "cosen" });
effect(() = > {
  console.log(obj.name);
});
obj.name = "senlin";
obj.name = "senlin1";
Copy the code

To do this, we need to do the three methods mentioned above:

  • effect
  • track
  • trigger

First, let’s sort out what Effect needs to do.

Thanks to the reactive() method, we now have a reactive data object that can be intercepted for every GET and set operation.

The effect() method needs to be able to trigger the effect callback whenever we modify the data.

The effect() method’s callback must return a reactive effect() function in order to execute after the data changes, so effect() returns a reactive effect inside.

Let’s look at the implementation of the effect method:

// Reactive side effects
function effect(fn) {
  const rxEffect = function () {
    try {
      // Catch an exception
      // Run fn and save effect
      activeEffectStacks.push(rxEffect);
      return fn();
    } finally{ activeEffectStacks.pop(); }};// It should be executed once by default
  rxEffect();
  // Return the response function
  return rxEffect;
}
Copy the code

Because Reactive and Effect are not yet linked, dependency collection has not yet taken place, so dependency collection will need to take place next.

Here we need to consider two questions:

1. When to collect dependencies?

2. How to collect dependencies and save dependencies?

First question: When to collect dependencies? We need to start collecting dependencies at value time, and this corresponds to value in the Proxy handlers get, in the createReactiveObject method above:

Get (Target, key, receiver) {// Reflect is a built-in object that provides methods for intercepting JavaScript operations. These methods are the same as the proxy Handlers method. let res = Reflect.get(target, key, receiver); // Collect dependencies/subscriptions to map current keys to effects+ track(target, key);Return isObject(res)? Return isObject(res)? Return isObject(res)? reactive(res) : res; },Copy the code

The corresponding trigger dependency is executed in the Proxy handlers GET:

Set (target, key, value, receiver) {// Let hasKey = hasOwn(target, key); let oldVal = target[key]; let res = Reflect.set(target, key, value, receiver); if (! HasKey) {console.log(" new properties ");+ trigger(target, "add", key);} else if (oldVal ! == value) {console.log(" modify attributes ");+ trigger(target, "set", key);
  }
  return res;
},
Copy the code

Then comes the second question: how do you collect dependencies and how do you save them? I actually marked this in the flow chart above:

{ target: { key: [fn1, fn2]; }}Copy the code

First of all, the dependency is effect function one by one. We can store it through the Set Set, which must correspond to a certain key of the object, that is, which effects depend on the corresponding value of a certain key in the object. The corresponding relationship can be saved through a Map object. That is:

targetMap: WeakMap{ target:Map{ key: Set[cb1,cb2...] }}Copy the code

When we value, we first take out the corresponding depsMap object from the global WeakMap object through the target object, and then obtain the corresponding DEP dependent set object according to the modified key, and then put the current effect into the DEP dependent set to complete the collection of dependencies. In fact, the corresponding method here is track:

function track(target, key) {
  // Get the top of the stack function
  let effect = activeEffectStacks[activeEffectStacks.length - 1];
  //
  if (effect) {
    // Get the dependency table for target
    let depsMap = targetsMap.get(target);
    if(! depsMap) { targetsMap.set(target, (depsMap =new Map()));
    }
    // Get the set of response functions corresponding to key
    let deps = depsMap.get(key);
    // Dynamically create dependencies
    if(! deps) { depsMap.set(key, (deps =new Set()));
    }
    if(! deps.has(effect)) { deps.add(effect); }}}Copy the code

When we modify the value, dependency update will be triggered, and the corresponding depMap object will be taken out from the global WeakMap object through the target object, and then the corresponding DEP dependency set will be taken out according to the modified key, and all effects in the set will be traversed and effect will be executed. The corresponding is the trigger method:

function trigger(target, type, key) {
  let depsMap = targetsMap.get(target);
  if (depsMap) {
    let deps = depsMap.get(key);
    if (deps) {
      // Execute the effects corresponding to the current key
      deps.forEach((effect) = >{ effect(); }); }}}Copy the code

The complete code

Put the code together here and test it with a demo at the end:

/** * Vue3 responsivity principle ** /

// Determine if it is an object
function isObject(val) {
  return typeof val === "object"&& val ! = =null;
}
function hasOwn(target, key) {
  return target.hasOwnProperty(key);
}
// WeakMap: Weakreference mapping table
// Original object: proxied object
let toProxy = new WeakMap(a);// Proxied object: original object
let toRaw = new WeakMap(a);// Reactive core methods
function reactive(target) {
  // Create a responsive object
  return createReactiveObject(target);
}
function createReactiveObject(target) {
  // If the object is not currently an object, just return it
  if(! isObject(target)) {return target;
  }
  // If already proxied, the proxied result is returned directly
  let proxy = toProxy.get(target);
  if (proxy) {
    return proxy;
  }
  // Prevent proxied objects from being proxied again
  if (toRaw.has(target)) {
    return target;
  }
  let baseHandler = {
    get(target, key, receiver) {
      // Reflect is a built-in object that provides methods to intercept JavaScript operations. These methods are the same as the proxy Handlers method.
      let res = Reflect.get(target, key, receiver);
      // Collect dependencies/subscriptions to map current keys to effects
      track(target, key);
      // If the value of get is an object, then it is recursive (this is an optimization compared to the default recursion in Vue2).
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      // We need to distinguish between new attributes and modified attributes
      let hasKey = hasOwn(target, key);
      let oldVal = target[key];
      let res = Reflect.set(target, key, value, receiver);
      if(! hasKey) {console.log("New Properties");
        trigger(target, "add", key);
      } else if(oldVal ! == value) {console.log("Modify properties");
        trigger(target, "set", key);
      }
      return res;
    },
    deleteProperty(target, key) {
      let res = Reflect.deleteProperty(target, key);
      returnres; }};let observed = new Proxy(target, baseHandler);
  toProxy.set(target, observed);
  toRaw.set(observed, target);
  return observed;
}

{name:[effect]}
let activeEffectStacks = [];
let targetsMap = new WeakMap(a);// If the target key changes, the array method is executed
function track(target, key) {
  // Get the top of the stack function
  let effect = activeEffectStacks[activeEffectStacks.length - 1];
  if (effect) {
    // Get the dependency table for target
    let depsMap = targetsMap.get(target);
    if(! depsMap) { targetsMap.set(target, (depsMap =new Map()));
    }
    // Get the set of response functions corresponding to key
    let deps = depsMap.get(key);
    // Dynamically create dependencies
    if(! deps) { depsMap.set(key, (deps =new Set()));
    }
    if(! deps.has(effect)) { deps.add(effect); }}}function trigger(target, type, key) {
  let depsMap = targetsMap.get(target);
  if (depsMap) {
    let deps = depsMap.get(key);
    if (deps) {
      // Execute the effects corresponding to the current key
      deps.forEach((effect) = >{ effect(); }); }}}// Reactive side effects
function effect(fn) {
  const rxEffect = function () {
    try {
      // Catch an exception
      // Run fn and save effect
      activeEffectStacks.push(rxEffect);
      return fn();
    } finally{ activeEffectStacks.pop(); }};// It should be executed once by default
  rxEffect();
  // Return the response function
  return rxEffect;
}

let obj = reactive({ name: "cosen" });
effect(() = > {
  console.log(obj.name);
});
obj.name = "senlin";
obj.name = "senlin";
Copy the code

By the way, post the results of the run:

We can see that the obj.name = “senlin” operation is executed twice, but the result is executed only once, which is related to toProxy and toRaw as defined in the code:

  • toProxy: storageThe original objecttoThe proxied objectIf already proxied, returns the proxied result
  • toRawstorageYestoThe original objectTo prevent proxied objects from being proxied again.

conclusion

Ok, here, I basically Vue3 on the responsive and dependent collection of related principles and we combed through, but also their own manual implementation of a simple pseudo-code.

This article is just a simple pseudo-code in the form of a demonstration, on the specific implementation details, if you want a more in-depth understanding, we can directly go to view Vue3 responsive part of the source.