Responsiveness of VUE

When I first started using VUE for project development, responsive data rendering was one of the features that surprised me the most. Let’s look at this code:

<body>
  <div id="app">
    <div>Modify the quantity of goods:<input type="number" v-model="product.quantity">
    </div>
    <div>Modify the price of goods:<input type="number" v-model="product.price">
    </div>

    <p>Total price: {{total}}</p>
  </div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script>
  const component = {
    data() {
      return {
        // Define a commodity object containing price and quantity
        product: {
          price: 10.quantity: 2}}},computed: {
      // Calculate the total price
      total() {
        return this.product.price * this.product.quantity
      }
    }
  }

  const app = Vue.createApp(component)
  app.mount('#app')
</script>
Copy the code

This is standard vue3 code, and totLA is always recalculated when you enter something in the input field, as shown below.

Such a function is called responsive.

Responsive data rendering is now a very important mechanism in the front end. But how exactly is this mechanism built step by step? That’s what this blog is about.

If you want to knowvue3Response system and build history, then you should read on.

Js procedural

To understand responsiveness, you need to understand programmatic. Let’s look at the following common js code:

  // Define a commodity object containing price and quantity
  let product = {
    price: 10.quantity: 2
  }
  / / total price
  let total = product.price * product.quantity;
  // First print
  console.log('Total price:${total}`); 
  // Modify the quantity of goods
  product.quantity = 5;
  // Print the second time
  console.log('Total price:${total}`); 
Copy the code

What should the above code print the first time? What should I print the second time?

Congratulations to you! That’s right, because it’s just plain js code, so it should print both times:

Total price: 20

But have you ever thought, when we go to print the second time, do you really want it to be 20?

Have you ever had the thought occur to you that if the quantity of goods changes, it would be great if the total price could follow itself? It’s human nature, and from a human point of view, it really should be that way. But the program is not so “smart.” So how can you make a program smarter?

The process of making a program “smart” is the process of responsive building.

You want to redo the operation when the data changes

You want to make your program smarter, so you start thinking, “If the data changes, I’ll just do it again.”

Just do it, and to do that, you begin to encapsulate the operations.

You define an anonymous function effect to calculate the total price of an item. And let Effect run before printing the total price. So you get the following code:

  // Define a commodity object containing price and quantity
  let product = {
    price: 10.quantity: 2
  }
  / / total price
  let total = 0;
  // An anonymous function to calculate the total price
  let effect = () = > {
    total = product.price * product.quantity;
  };
  // First print
  effect();
  console.log('Total price:${total}`); // Total price: 20
  // Modify the quantity of goods
  product.quantity = 5;
  // Print the second time
  effect();
  console.log('Total price:${total}`); // Total price: 50
Copy the code

In such a code, you get the desired result: the data changes and the operation is re-executed

However, you soon discover a new problem: such code can only maintain a single total price calculation. You want it to support more computations, so what do you do?

You want to redo multiple operations when the data changes

Your code supports only one operation, and you want it to support more.

To do this, you begin a simple wrapper around your code. You do three things:

  1. createSetArray (Click to learn about Set), used to store multiple operation functions
  2. createtrackFunction, which is used to evaluateSetTo store the operation functions
  3. createtriggerFunction to perform all of the operation functions

So you get the following code, and call this set of code reactive:

  // ------------- create responsive -------------
  // Set array, used to hold all operation functions
  let deps = new Set(a);// Save the operation function
  function track() {
    deps.add(effect);
  }
  // trigger, execute all operation functions
  function trigger() {
    deps.forEach((effect) = > effect());
  }

  // ------------- Create data source -------------
  // Declare the commodity object as the data source
  let product = {
    price: 10.quantity: 2
  };
  // State the total price
  let total = 0;
  // Calculate the total price of the anonymous function
  let effect = () = > {
    total = product.price * product.quantity;
  };

  // ------------- Perform reactive -------------
  // Save the operation function
  track();
  // Calculate the total price
  effect();
  console.log('Total price:${total}`); // Total price: 20
	
  // Modify the data source
  product.quantity = 5;
  // Data source is modified, trigger is executed, recalculate all total
  trigger();
  console.log('Total price:${total}`); // Total price: 50
Copy the code

You’re so proud of your creation that you start recommending it to your friends. Soon, however, a problem was raised: I wanted to apply responsiveness to a specific property of the object, not a property change and rerunce the entire calculation.

You want to: make each property individually responsive

Reactive binding object that causes a property change and rerun of all calculations. So you want to apply responsiveness to a specific property of an object, only recalculating the content associated with that property

To do this, you need to use a Map object.

A Map stores data in the form of key:val. You want the attribute to be the key and the set of operations associated with that attribute to be val. From this you construct a depsMap object for your purpose:

// ------------- create responsive -------------
  // Key: collection of Val structures
  let depsMap = new Map(a);// Store the operator function separately for each attribute so that each attribute has its own independent response
  function track(key, eff) {
    let dep = depsMap.get(key)
    if(! dep) { depsMap.set(key, (dep =new Set()))
    }
    dep.add(eff)
  }
  // trigger that executes the operation function for the specified property
  function trigger(key) {
    // Get the deP array of the specified function
    const dep = depsMap.get(key);
    // Iterate through the dep, executing the operator function of the specified function
    if (dep) {
      dep.forEach((eff) = >eff()); }}// ------------- Create data source -------------
  // Declare the commodity object as the data source
  let product = {
    price: 10.quantity: 2
  };
  // State the total price
  let total = 0;
  // Calculate the total price of the anonymous function
  let effect = () = > {
    total = product.price * product.quantity;
  };

  // ------------- Perform reactive -------------
  // Save the operation function
  track('quantity', effect);
  // Calculate the total price
  effect();
  console.log('Total price:${total}`); // Total price: 20

  // Modify the data source
  product.quantity = 5;
  // quantity is modified to trigger only the response of Quantity
  trigger('quantity');
  console.log('Total price:${total}`); // Total price: 50
</script>
Copy the code

Your client is always very picky, and soon they come up with a new problem: My program can’t have just one object! You need to make all objects responsive!

You want to make different properties of different objects individually responsive

Your reactive style needs to override all objects in your program, or your code will be meaningless!

To do this, you need to cache objects, attributes, and operations separately, which depsMap doesn’t do anymore. You need a more powerful Map so that every object has a Map. It is WeakMap.

A WeakMap object is a set of key/value pairs. The key must be an object, and the value can be arbitrary.

With WeakMap you have a depsMap for each object:

  // ------------- create responsive -------------
  WeakMap: Key must be an object, and val can be any value
  const targetMap = new WeakMap(a)// Store the operation function separately for each attribute of the different object, so that each attribute of the different object has its own independent response
  function track(target, key, eff) {
    // Get the depsMap of the object
    let depsMap = targetMap.get(target)
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the depsMap attribute
    let dep = depsMap.get(key)
    if(! dep) { depsMap.set(key, (dep =new Set()))}// Save different object, different attributes of the operation function
    dep.add(eff)
  }
  // trigger that executes the operation on the specified property of the specified object
  function trigger(target, key) {
    // Get the depsMap of the object
    let depsMap = targetMap.get(target)
    if(! depsMap) {return
    }
    // Get the deP array of the specified function
    const dep = depsMap.get(key);
    // Iterate through the dep, executing the operator function of the specified function
    if (dep) {
      dep.forEach((eff) = >eff()); }}// ------------- Create data source -------------
  // Declare the commodity object as the data source
  let product = {
    price: 10.quantity: 2
  };
  // State the total price
  let total = 0;
  // Calculate the total price of the anonymous function
  let effect = () = > {
    total = product.price * product.quantity;
  };


  // ------------- Perform reactive -------------
  // Save the operation function
  track(product, 'quantity', effect);
  // Calculate the total price
  effect();
  console.log('Total price:${total}`); // Total price: 20

  // Modify the data source
  product.quantity = 5;
  // quantity is modified to trigger only the response of Quantity
  trigger(product, 'quantity');
  console.log('Total price:${total}`); // Total price: 50
Copy the code

I need to re-execute the trigger every time the data changes, which is too much trouble! What if I forget? . Customers will always put forward some requirements to change (wu) into (Li), there is no way, who let people are customers?

You want to: make different properties of different objects automatically responsive

I need to re-execute the trigger every time the data changes, your client complains.

To do this, you need to understand the “behavior of data” : you need to know when data is assigned and when it is output.

At this point you need to use two new objects:

  • Proxy (here’s a video version of Happy)
  • Reflect

With Proxy + Reflect you have successfully implemented listening on data:

  // ------------- create responsive -------------
  WeakMap: Key must be an object, and val can be any value
  const targetMap = new WeakMap(a)// Store the operation function separately for each attribute of the different object, so that each attribute of the different object has its own independent response
  function track(target, key, eff) {
    // Get the depsMap of the object
    let depsMap = targetMap.get(target)
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the depsMap attribute
    let dep = depsMap.get(key)
    if(! dep) { depsMap.set(key, (dep =new Set()))}// Save different object, different attributes of the operation function
    dep.add(eff)
  }
  // trigger that executes the operation on the specified property of the specified object
  function trigger(target, key) {
    // Get the depsMap of the object
    let depsMap = targetMap.get(target)
    if(! depsMap) {return
    }
    // Get the deP array of the specified function
    const dep = depsMap.get(key);
    // Iterate through the dep, executing the operator function of the specified function
    if (dep) {
      dep.forEach((eff) = >eff()); }}// Use proxy to delegate data sources for listening purposes
  function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        track(target, key, effect)
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if(result && oldValue ! = value) { trigger(target, key) }return result
      },
    }
    return new Proxy(target, handlers)
  }

  // ------------- Create data source -------------

  // Declare the commodity object as the data source
  let product = reactive({ price: 10.quantity: 2 })
  // State the total price
  let total = 0;
  // Calculate the total price of the anonymous function
  let effect = () = > {
    total = product.price * product.quantity;
  };


  // ------------- Perform reactive -------------
  effect()
  console.log('Total price:${total}`); // Total price: 20
  // Modify the data source
  product.quantity = 5;
  console.log('Total price:${total}`); // Total price: 50
Copy the code

You feel satisfied that your code is perfect. All of a sudden, I hear a voice saying, “Don’t you think every time you perform an effect, it’s anti-human?”

You want: to automate the operations

Automation! Automation! All operations should run automatically!

To make the operation run automatically, you design an effect function that takes the operation and executes it automatically

  // ------------- create responsive -------------
  WeakMap: Key must be an object, and val can be any value
  const targetMap = new WeakMap(a)// The object of the operation function
  let activeEffect = null;
  // Store the operation function separately for each attribute of the different object, so that each attribute of the different object has its own independent response
  function track(target, key) {
    if (activeEffect) {
      // Get the depsMap of the object
      let depsMap = targetMap.get(target)
      if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the depsMap attribute
      let dep = depsMap.get(key)
      if(! dep) { depsMap.set(key, (dep =new Set()))}// Save different object, different attributes of the operation function
      dep.add(activeEffect)
    }

  }
  // trigger that executes the operation on the specified property of the specified object
  function trigger(target, key) {
    // Get the depsMap of the object
    let depsMap = targetMap.get(target)
    if(! depsMap) {return
    }
    // Get the deP array of the specified function
    const dep = depsMap.get(key);
    // Iterate through the dep, executing the operator function of the specified function
    if (dep) {
      dep.forEach((eff) = >eff()); }}// Use proxy to delegate data sources for listening purposes
  function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        track(target, key)
        return Reflect.get(target, key, receiver)
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if(result && oldValue ! = value) { trigger(target, key) }return result
      },
    }
    return new Proxy(target, handlers)
  }

  // Receive operation function, execute operation function
  function effect(eff) {
    activeEffect = eff;
    activeEffect();
    activeEffect = null;
  }

  // ------------- Create data source -------------
  // Declare the commodity object as the data source
  let product = reactive({ price: 10.quantity: 2 })
  // State the total price
  let total = 0;
  // Calculate the total price by effect
  effect(() = > {
    total = product.price * product.quantity;
  })


  // ------------- Perform reactive -------------
  console.log('Total price:${total}`); // Total price: 20
  // Modify the data source
  product.quantity = 5;
  console.log('Total price:${total}`); // Total price: 50
Copy the code

conclusion

Vue is surprisingly responsive, and we want to know more about it, and more importantly, how it has evolved.

We start from the JS procedural, standing in the human nature began to think, what should the program look like?

We went through six major phases to get to the responsive system we wanted, and this is exactly what vuE3’s responsive system went through when it was built.