0.Reactivity basic concepts

This chapter is an unoriginal excerpt from the official VUE documentation and a translation of the reactive teaching section on VUE-Master. It is written simply because I feel it is important to understand the concepts before reading the reactive code. If you already know something about it, you can skip this chapter.

What is reactivity?

This term is often used in programming, but what does it mean? Responsiveness is a programming paradigm that allows us to adapt to change in a declarative way. The typical example people usually show is an Excel spreadsheet (a very good example).

If you put the number 2 in the first cell and the number 3 in the second cell and ask for SUM, the spreadsheet will calculate it for you. Don’t be surprised, also, if you update the first number, the SUM will update automatically.

In React

Let’s look at an example of responsiveness in react:

class List extends React.Component {
  constructor() {
    this.state = {
      price: 5.00.quantity: 2}; } addOne =() = > {
    this.setState('quantity'.this.quantity+1);
  }
  render() {
    return (
      <ul>
        <li>price: {this.state.price}</li>
        <li>sum: {this.state.price * this.state.quanity}</li>
        <button onclick={addOne}>add one</button>
      </ul>)}}Copy the code

{price * quanity}} {price * quanity}} {price * quanity}} {price * quanity}}} {price * quanity}} {price * quanity}}} {price * quanity}}} {price * quanity}}} {price * quanity}}

When this.setState is called, an update task is created and added to the React scheduler. The Scheduler will rebuild fiberTree from the component root and determine whether to update the DOM.

In Vue

After we know the update process of React, we can find that react does not detect all the related values of the quantity when updating the quantity, but re-renders the component tree (part of rendering) according to the new values. Of course, the construction of React Fiber is actually very complex and efficient.

Let’s look at a more responsive example of vue:

<div id="app"> <div>Price: ${{ product.price }}</div> <div>Total: ${{ product.price * product.quantity }}</div> <div>Taxes: ${{ totalPriceWithTax }}</div> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script> var vm = new Vue({el: '#app', data: {product: {price: 5.00, quantity: 2}}, computed: {totalPriceWithTax() {return this.product.price * this.product.quantity * 1.03}}}) </script>Copy the code

This example has a computed property in it. Vue’s Reactivity system knows that if price changes, it should do three things:

  • Update pagepricePrice display.
  • recalculatepriceMultiplied by thequantityAnd update the page.
  • Call againtotalPriceWithTaxFunction and update the page.

But wait, how does Vue’s Reactivity system know what to update when prices change, and how does it keep track of everything? Because the update process above is not how javascript programs work:

let product = { price: 5.quantity: 2 }
let total = product.price * product.quantity  // 10 right?
product.price = 20
console.log(`total is ${total}`)
Copy the code

What do you think it prints? Since we are not using Vue, it will print 10. In Vue, we want to update totals when prices or quantities are updated. We want 40 as a real-time result.

Now that we know what we want to achieve, let’s take a look at how Vue’s Reactivity implementation works.

Track And Trigger Effects

First of all, we define three concepts:

  • effect: Side effects, which are the effects on other individuals of a reactive object after its properties or properties change.
  • TrackTracing, which is the continuous recording of the side effects of a reactive object property.
  • Trigger: trigger, when the object itself/property changes, we need to trigger beforetrackAll of theeffect.

The realization of the above three points is equivalent to the realization of a responsive system. First, let’s take a look at how to manually build effect and Track and trigger.

Effect & Track & Trigger

First, we need some way to tell our application, “Store the code I’m going to run (effect), I may need you to run it another time.” Then we run the code to initialize the calculation. If the price or quantity variables are updated, run the stored code again.We can do this by recording the functioneffect) to do this in order to run it again:

let product = { price: 5.quantity: 2 }
let total = 0

let effect = () = > { total = product.price * product.quantity }

effect() // Also go ahead and run it
Copy the code

The next thing we need to implement is the track function to store our effects:

track()  // Remember this in case we want to run it later
Copy the code

To implement track, we need a place to store our defined effects. We create a DEP variable as a dependency.

We call them dependencies because normally in observer mode, dependencies have subscribers (in this case effects) who are notified when the object changes state. Since all a dependency needs to store is a Set of effects, we can simply create a Set.

let dep = new Set(a)// Our object tracking a list of effects
Copy the code

Our track function can then simply add our effect to this dependency:

function track () {
  dep.add(effect) // Store the current effect
}
Copy the code

Next, we’ll write a trigger function to run all the effects we’ve stored in the DEP.

function trigger() { 
  dep.forEach(effect= > effect()) 
}
Copy the code

Very simple, right? Now we have manually implemented a responsive effect, let’s look at the complete code:

let product = { price: 5.quantity: 2 }
let total = 0
let dep = new Set(a)function track() {
  dep.add(effect)
}

function trigger() {
  dep.forEach(effect= > effect())
}

let effect = () = > {
  total = product.price * product.quantity
}

track()
trigger()

product.price = 20
console.log(total) / / = > 10

trigger()
console.log(total) / / = > 40
Copy the code

Multiple Properties

That’s not the end of the problem. Our ReActivity object has different properties, each of which needs its own DEP (or set of effects). For example, product = {price: 5, quantity: 2}, the price attribute requires its own DEP, as does Quantity.

It is easy to store the DEPs of different attributes. We think of a data structure called map, so we create a depsMap, which is of type map:

Now we call each time we change the value of the propertytracktriggerEach requires a new parameter, the property name. The code above will be rewritten as follows:

const depsMap = new Map(a)function track(key, effect) {
  // Make sure this effect is being tracked.
  let dep = depsMap.get(key) // Get the current dep (effects) that need to be run when this key (property) is set
  if(! dep) {// There is no dep (effects) on this key yet
    depsMap.set(key, (dep = new Set())) // Create a new Set
  }
  dep.add(effect) // Add effect to dep}}function trigger(key) {
  let dep = depsMap.get(key) // Get the dep (effects) associated with this key
  if (dep) { // If they exist
    dep.forEach(effect= > {
      // run them all
      effect()
    })
  }
}
Copy the code

Multiple Reactive Objects

We just solved the problem of storing the DEP of the different properties of the Reactivity object, but we don’t necessarily have multiple ReActivity objects in the program.

Now we need another Map to store depsMap for each object. Unlike depMap, we need to use WeakMap to store reActivity objects. WeakMap is an extension of JavaScript Map that uses only objects as keys.

The following figure shows this storage structure, usedWeakMapThe advantage is that objects stored by their key names are not tracked by the garbage collector, that is, they do not participate in the count. The key name is a weak reference to an object (which is not taken into account by garbage collection), and when the object referenced by the key name is collected,WeakMapAutomatically removes the corresponding key-value pair.Now we calltracktriggerWe now need to know which object we are targeting. Here is the modified code:

const targetMap = new WeakMap(a)// targetMap stores the effects that each object should re-run when it's updated

function track(target, key) {
  // We need to make sure this effect is being tracked.
  let depsMap = targetMap.get(target) // Get the current depsMap for this target

  if(! depsMap) {// There is no map.
    targetMap.set(target, (depsMap = new Map())) // Create one
  }

  let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
  if(! dep) {// There is no dependencies (effects)
    depsMap.set(key, (dep = new Set())) // Create a new Set
  }

  dep.add(effect) // Add effect to dependency map
}

function trigger(target, key) {
  const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
  if(! depsMap) {return
  }

  let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
  if (dep) {
    dep.forEach(effect= > {
      // run them all
      effect()
    })
  }
}
Copy the code

We now have a very efficient way to track the dependencies of multiple Objects, which is the first step in building a reactive system.

True Reactivity In Vue3

Above, we manually call track and trigger to achieve responsiveness. In actual development, we certainly need such process automation. At this time, we need a hook corresponding to the object attributes get and set methods. To make the program automatically call predefined functions when a property changes to achieve responsiveness.

  • GET property => We need to GET propertytrackThis is generated by the fetch operationeffect.
  • SET Property => We need to trigger any trace dependencies for this property.

Vue 3 uses ES6 Proxy and Reflect to intercept GET and SET calls. Previous Vue 2 did this using Object.defineProperty. Let’s look at the implementation:

Proxy & Reflect

In this section, you need to know a little bit about Proxy. Without going into detail, what you really need to know is that a Proxy is an object that contains another object or function and allows you to intercept it.

const proxyObj = new Proxy(obj, {
    get(target, key, receiver) {
        return Reflect.get(target, key, receiver);
    },
    set(target, key, val, receiver) {
        return Reflect.set(target, key, val, receiver);
    },
    deleteProperty(target, key) {
        return Reflect.deleteProperty(target, key); }});Copy the code

Proxy allows you to intercept objects when they are being used, set, or deleted. It is usually used in conjunction with Reflect. Note that Reflect only exists to normalize object property operations and support functional programming.

+ Reflect.set(target, key, val, receiver) 
- target[key] = val
+ Reflect.deleteProperty(target, key) 
- delete target.key
Copy the code

Combina proxy with Track&Trigger

What we need to do next is to combine Track&Trigger with proxy to achieve automatic Reactive. The overall idea is very clear. Proxy intercepts get&set operations of object attributes. Then add track&tigger in the appropriate place for responsiveness as follows:

const targetMap = new WeakMap(a)// targetMap stores the effects that each object should re-run when it's updated
function track(target, key) {
  // We need to make sure this effect is being tracked.
  let depsMap = targetMap.get(target) // Get the current depsMap for this target
  if(! depsMap) {// There is no map.
    targetMap.set(target, (depsMap = new Map())) // Create one
  }
  let dep = depsMap.get(key) 
  // Get the current dependencies (effects) that need to be run when this is set
  if(! dep) {// There is no dependencies (effects)
    depsMap.set(key, (dep = new Set())) 
    // Create a new Set
  }
  dep.add(effect) 
  // Add effect to dependency map
}

function trigger(target, key) {
  const depsMap = targetMap.get(target) 
  // Does this object have any properties that have dependencies (effects)
  if(! depsMap) {return
  }
  let dep = depsMap.get(key) 
  // If there are dependencies (effects) associated with this
  if (dep) {
    dep.forEach(effect= > {
      // run them all
      effect()
    })
  }
}

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver)
      track(target, key) 
      // If this reactive property (target) is GET inside then track the effect to rerun on SET
      return result
    },
    set(target, key, value, receiver) {
      let oldValue = target[key]
      let result = Reflect.set(target, key, value, receiver)
      if(result && oldValue ! = value) { trigger(target, key)// If this reactive property (target) has effects to rerun on SET, trigger them.
      }
      return result
    }
  }
  return new Proxy(target, handler)
}
Copy the code

To see how this works, we change product to a responsive object. When we get the property of this object, we will track effect. In this case, the effect definition will execute immediately and trigger track in the getter of the object. Product. Quantity = 3 triggers the trigger on the setter to trigger the effect just after track:

let product = reactive({ price: 5.quantity: 2 })
let total = 0

let effect = () = > {
  total = product.price * product.quantity
}
effect()

console.log('before updated quantity total = ' + total) / / 10
product.quantity = 3
console.log('after updated quantity total = ' + total)  / / 15
Copy the code

Nested reactivity

One last little detail, when a nested object is accessed from a responsive proxy, the object is also converted to a proxy before being returned:

const handler = {
  get(target, property, receiver) {
    track(target, property)
    const value = Reflect.get(... arguments)if (isObject(value)) {
      // Wrap nested objects in its own reactive proxy
      return reactive(value)
    } else {
      return value
    }
  }
  // ...
}
Copy the code