The responsive principle in Vue3 is very important. By learning the responsive principle of Vue3, we can not only learn some design patterns and ideas of VUue. Js, but also help us improve the efficiency of project development and code debugging ability.

Before this, I also wrote a “explore Vue. Js response type principle”, mainly introduces the principle of Vue 2 response type, this article supplement Vue 3.

I recently relearned my knowledge of Vue3 Reactivity on Vue Mastery, this time with even greater success. This article will take you to learn from scratch how to achieve a simple version of Vue 3 responsive, to help you understand its core, read Vue 3 responsive related source code can be more handy.

Vue 3 is used in response mode

1. Use in Vue 3

As we look at Vue 3, we can use a simple example to see what responsiveness is in Vue 3:

<! -- HTML content -->
<div id="app">
    <div>Price: {{price}}</div>
    <div>Total: {{price * quantity}}</div>
    <div>getTotal: {{getTotal}}</div>
</div>
Copy the code
const app = Vue.createApp({ // ① Create an APP instance
    data() {
        return {
            price: 10.quantity: 2}},computed: {
        getTotal() {
            return this.price * this.quantity * 1.1
        }
    }
})
app.mount('#app')  // Mount the APP instance
Copy the code

Create APP instance and mount APP instance, then you can see the corresponding values displayed in the page respectively:

When we change the price or quantity values, the content will show the results of the changes where they are referenced on the page. At this point, we wonder why when the data changes, the relevant data also changes, so let’s move on.

2. Implement the response formula of a single value

In normal JS code execution, there are no responsive changes, such as executing the following code on the console:

let price = 10, quantity = 2;
const total = price * quantity;
console.log(`total: ${total}`); // total: 20
price = 20;
console.log(`total: ${total}`); // total: 20
Copy the code

As you can see from this, the value of total does not change after changing the value of the price variable.

So how do you change the above code so that Total updates automatically? We can actually save the method that changes the total value and wait until the variable associated with the total value (such as the value of the Price or quantity variables) changes to trigger the method and update the total. We can do this:

let price = 10, quantity = 2, total = 0;
const dep = new Set(a);/ / 1.
const effect = () = > { total = price * quantity };
const track = () = > { dep.add(effect) };  / / 2.
const trigger = () = > { dep.forEach( effect= > effect() )};  / / 3.

track();
console.log(`total: ${total}`); // total: 0
trigger();
console.log(`total: ${total}`); // total: 20
price = 20;
trigger();
console.log(`total: ${total}`); // total: 40
Copy the code

The above code implements responsive changes to total data in three steps:

Initialize a deP variable of type Set, which is used to store the effect function to be executed.

(2) Create the track() function, which is used to save the side effects that need to be performed in the DEP variable (also called collect side effects);

③ Create trigger() function to execute all side effects in deP variable;

In each modificationpricequantityAfter the calltrigger()After the function has performed all of its side effects,totalThe value is automatically updated to the latest value.

(Image credit: Vue Mastery)

3. Realize the response of a single object

Typically, our objects have multiple attributes, and each attribute requires its own DEP. How do we store this? Such as:

let product = { price: 10.quantity: 2 };
Copy the code

As we know from the previous introduction, we store all side effects in a Set that has no duplicates. Here we introduce a Set of type Map (i.e. DepsMap) whose key is an attribute of the object (e.g. Price attribute), value is the previous Set to save side effects (such as DEP object), the general structure is shown as follows:

(Image credit: Vue Mastery)

Implementation code:

let product = { price: 10.quantity: 2 }, total = 0;
const depsMap = new Map(a);/ / 1.
const effect = () = > { total = product.price * product.quantity };
const track = key= > {     / / 2.
	let dep = depsMap.get(key);
  if(! dep) { depsMap.set(key, (dep =new Set()));
  }
	dep.add(effect);
}

const trigger = key= > {  / / 3.
	let dep = depsMap.get(key);
  if(dep) {
		dep.forEach( effect= >effect() ); }}; track('price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger('price');
console.log(`total: ${total}`); // total: 40
Copy the code

The above code implements responsive changes to total data in three steps:

Initialize a depsMap variable of type Map, which holds each object attribute that needs to be changed in a responsive manner.

(2) Create a track() function to save the side effects to be performed in the depsMap variable under the corresponding object attributes (also called collect side effects);

③ Create trigger() functions to execute all side effects of the object attributes specified in the DEP variable;

In this way, the listener responds to changes in the property value of the Product object, and the total value is updated accordingly.

4. Realize multiple object responsivity

If we have multiple responsive data, such as object A and object B, how do we keep track of each responding object?

Here, we introduce a WeakMap type object, which takes the object to be observed as the key, and the value is the Map variable used to save the object attributes. The code is as follows:

let product = { price: 10.quantity: 2 }, total = 0;
const targetMap = new WeakMap(a);// ① Initialize targetMap and save the observed object
const effect = () = > { total = product.price * product.quantity };
const track = (target, key) = > {     // collect dependencies
  let depsMap = targetMap.get(target);
  if(! depsMap){ targetMap.set(target, (depsMap =new Map()));
  }
	let dep = depsMap.get(key);
  if(! dep) { depsMap.set(key, (dep =new Set()));
  }
	dep.add(effect);
}

const trigger = (target, key) = > {  // execute all side effects of the specified attributes of the specified object
  const depsMap = targetMap.get(target);
  if(! depsMap)return;
	let dep = depsMap.get(key);
  if(dep) {
		dep.forEach( effect= >effect() ); }}; track(product,'price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger(product, 'price');
console.log(`total: ${total}`); // total: 40
Copy the code

The above code implements responsive changes to total data in three steps:

Initialize a targetMap variable of WeakMap type, which is used to observe each responsive object;

② Create the track() function, which is used to save the side effects to be performed in the dependency of the specified object (target) (also called collect side effects);

(3) Create trigger() function, which is used to execute all side effects of the specified property in the specified object (target);

In this way, the listener responds to changes in the property value of the Product object, and the total value is updated accordingly.

The general process is as follows:

(Image credit: Vue Mastery)

Proxy and Reflect

In the previous section, we introduced how to update data automatically after data changes, but the problem is that each time you need to manually collect dependencies by triggering the track() function and perform all side effects through the trigger() function to achieve the data update purpose.

This section addresses this problem by implementing automatic calls to both functions.

1. How to achieve automatic operation

Here we introduce the concept of JS object accessors, the solution is as follows:

  • Automatically executed when data is readtrack()Function automatically collects dependencies;
  • Automatically executed when data is modifiedtrigger()Function performs all side effects;

So how do you intercept GET and SET operations? Let’s see how Vue2 and Vue3 are implemented:

  • In Vue2, it is implemented using ES5’s object.defineProperty () function;
  • In Vue3, ES6’s Proxy and Reflect apis are used.

Note that the Proxy and Reflect apis used by Vue3 do not support IE.

The Object.defineProperty() function won’t be explained much, you can read the documentation, but the Proxy and Reflect apis will be the focus of the rest of the article.

2. Enrollment: 503

There are generally three ways to read an object’s properties:

  1. use.Operator:leo.name
  2. use[]leo['name']
  3. useReflectAPI:Reflect.get(leo, 'name')

All three methods have the same output.

3. How to use Proxy

Proxy objects are used to create a Proxy for an object to intercept and customize basic operations (such as property lookup, assignment, enumeration, function calls, and so on). The syntax is as follows:

const p = new Proxy(target, handler)
Copy the code

The parameters are as follows:

  • Target: The target object (which can be any type of object, including a native array, a function, or even another Proxy) to be wrapped with a Proxy.
  • Handler: An object that usually has functions as properties, and the functions in each property define the agents that perform the various operationspBehavior.

Let’s take a look at the Proxy API through the official documentation:

let product = { price: 10.quantity: 2 };
let proxiedProduct = new Proxy(product, {
	get(target, key){
  	console.log('Data being read:',key);
    returntarget[key]; }})console.log(proxiedProduct.price); 
// Data being read: price
/ / 10
Copy the code

This ensures that every time we read proxiedProduct.price we execute the get handler of the proxiedProduct.price. The process is as follows:

(Image credit: Vue Mastery)

Then use this in conjunction with Reflect, just modify the get function:

	get(target, key, receiver){
  	console.log('Data being read:',key);
    return Reflect.get(target, key, receiver);
  }
Copy the code

The output is the same.

Add the set function to intercept the modification of the object:

let product = { price: 10.quantity: 2 };
let proxiedProduct = new Proxy(product, {
	get(target, key, receiver){
  	console.log('Data being read:',key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver){
  	console.log('Data being modified:', key, ', the value is: ', value);
  	return Reflect.set(target, key, value, receiver);
  }
})
proxiedProduct.price = 20;
console.log(proxiedProduct.price); 
// Data being modified: price, value: 20
// Data being read: price
/ / 20
Copy the code

This completes the get and set functions that intercept the reading and modification of objects. To compare the Vue3 source code, let’s abstract the above code to make it look more like the Vue3 source code:

function reactive(target){
	const handler = {  // encapsulate the unified handler object
  	get(target, key, receiver){
      console.log('Data being read:',key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver){
      console.log('Data being modified:', key, ', the value is: ', value);
      return Reflect.set(target, key, value, receiver); }}return new Proxy(target, handler); // call the Proxy API
}

let product = reactive({price: 10.quantity: 2}); // convert the object to a responsive object
product.price = 20;
console.log(product.price); 
// Data being modified: price, value: 20
// Data being read: price
/ / 20
Copy the code

The output remains the same.

4. Modify the track and trigger functions

Using the code above, we have implemented a simple reactive() function that transforms ordinary objects into reactive objects. The track() and trigger() functions are still missing.

const targetMap = new WeakMap(a);let total = 0;
const effect = () = > { total = product.price * product.quantity };
const track = (target, key) = > { 
  let depsMap = targetMap.get(target);
  if(! depsMap){ targetMap.set(target, (depsMap =new Map()));
  }
	let dep = depsMap.get(key);
  if(! dep) { depsMap.set(key, (dep =new Set()));
  }
	dep.add(effect);
}

const trigger = (target, key) = > {
  const depsMap = targetMap.get(target);
  if(! depsMap)return;
	let dep = depsMap.get(key);
  if(dep) {
		dep.forEach( effect= >effect() ); }};const reactive = (target) = > {
	const handler = {
  	get(target, key, receiver){
      console.log('Data being read:',key);
      const result = Reflect.get(target, key, receiver);
      track(target, key);  // Automatically call the track method to collect dependencies
      return result;
    },
    set(target, key, value, receiver){
      console.log('Data being modified:', key, ', the value is: ', value);
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if(oldValue ! = result){ trigger(target, key);// The trigger method is automatically called to execute the dependency
      }
      returnresult; }}return new Proxy(target, handler);
}

let product = reactive({price: 10.quantity: 2}); 
effect();
console.log(total); 
product.price = 20;
console.log(total); 
// Data being read: price
// Data being read: quantity
/ / 20
// Data being modified: price, value: 20
// Data being read: price
// Data being read: quantity
/ / 40
Copy the code

(Image credit: Vue Mastery)

ActiveEffect and REF

In the previous code section, there was another problem: The dependencies in the track function were defined externally, and the track function had to manually change the method names of its dependencies whenever they were collected.

For example, if the dependency is now foo, we need to change the logic of the track function, perhaps like this:

const foo = () = > { / * * / };
const track = (target, key) = > {     / / 2.
  // ...
	dep.add(foo);
}
Copy the code

So how to solve this problem?

1. Introduce activeEffect variables

Next, the activeEffect variable is introduced to hold the currently running Effect function.

let activeEffect = null;
const effect = eff= > {
	activeEffect = eff; // 1. Assign eff to activeEffect
  activeEffect();     // 2. Run activeEffect
  activeEffect = null;// 3. Reset activeEffect
}
Copy the code

Then use the activeEffect variable as a dependency in the track function:

const track = (target, key) = > {
    if (activeEffect) {  // 1. Check whether the activeEffect exists
        let depsMap = targetMap.get(target);
        if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
        }
        let dep = depsMap.get(key);
        if(! dep) { depsMap.set(key, (dep =new Set()));
        }
        dep.add(activeEffect);  // 2. Add activeEffect dependencies}}Copy the code

Change the usage mode to:

effect(() = > {
    total = product.price * product.quantity
});
Copy the code

This solves the problem of manually changing dependencies, which is how Vue3 solves this problem. After completing the test code, it looks like this:

const targetMap = new WeakMap(a);let activeEffect = null; // Introduce the activeEffect variable

const effect = eff= > {
	activeEffect = eff; // 1. Assign the side effect to activeEffect
  activeEffect();     // 2. Run activeEffect
  activeEffect = null;// 3. Reset activeEffect
}

const track = (target, key) = > {
    if (activeEffect) {  // 1. Check whether the activeEffect exists
        let depsMap = targetMap.get(target);
        if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
        }
        let dep = depsMap.get(key);
        if(! dep) { depsMap.set(key, (dep =new Set()));
        }
        dep.add(activeEffect);  // 2. Add activeEffect dependencies}}const trigger = (target, key) = > {
    const depsMap = targetMap.get(target);
    if(! depsMap)return;
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(effect= >effect()); }};const reactive = (target) = > {
    const handler = {
        get(target, key, receiver) {
            const result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            const oldValue = target[key];
            const result = Reflect.set(target, key, value, receiver);
            if(oldValue ! = result) { trigger(target, key); }returnresult; }}return new Proxy(target, handler);
}

let product = reactive({ price: 10.quantity: 2 });
let total = 0, salePrice = 0;
// Change the way effect is used to pass side effects as parameters to the effect method
effect(() = > {
    total = product.price * product.quantity
});
effect(() = > {
    salePrice = product.price * 0.9
});
console.log(total, salePrice);  / / 20 9
product.quantity = 5;
console.log(total, salePrice);  / / 50 9
product.price = 20;
console.log(total, salePrice);  / / 18 100
Copy the code

Consider what would happen if the first effect function was product.price instead of salePrice:

effect(() = > {
    total = salePrice * product.quantity
});
effect(() = > {
    salePrice = product.price * 0.9
});
console.log(total, salePrice);  / / 0 9
product.quantity = 5;
console.log(total, salePrice);  / / 45 9
product.price = 20;
console.log(total, salePrice);  / / 45 18
Copy the code

The result is completely different because salePrice does not change in response, but requires a call to the second effect function, the value of the product.price variable.

Code address: github.com/Code-Pop/vu…

2. Introduce ref method

Those familiar with the Vue3 Composition API may recall Ref, which takes in a value and returns a reactive mutable Ref object whose value can be obtained via the value property.

Ref: Takes an internal value and returns a responsive and mutable REF object. The ref object has a single property.value pointing to an internal value.

The following is an example on the official website:

const count = ref(0)
console.log(count.value) / / 0

count.value++
console.log(count.value) / / 1
Copy the code

There are two ways to implement the ref function:

  1. userectivefunction
const ref = intialValue= > reactive({value: intialValue});
Copy the code

This is ok, although Vue3 is not implemented this way.

  1. Use property accessors for objects (evaluate properties)

Property methods include getters and setters.

const ref = raw= > {
	const r = {
  	get value(){
    	track(r, 'value');
      return raw;
    },
    
    set value(newVal){
    	raw = newVal;
      trigger(r, 'value'); }}return r;
}
Copy the code

The usage is as follows:

let product = reactive({ price: 10.quantity: 2 });
let total = 0, salePrice = ref(0);
effect(() = > {
    salePrice.value = product.price * 0.9
});
effect(() = > {
    total = salePrice.value * product.quantity
});
console.log(total, salePrice.value); / / September 18
product.quantity = 5;
console.log(total, salePrice.value); / / 45 9
product.price = 20;
console.log(total, salePrice.value); / / 18 90
Copy the code

This is also at the heart of the REF implementation in Vue3.

Code address: github.com/Code-Pop/vu…

Implement simple Computed methods

Those of you who have used Vue might wonder why the salePrice and total variables above are not computed.

Yeah, that works, so let’s do a simple computed method.

const computed = getter= > {
    let result = ref();
    effect(() = > result.value = getter());
    return result;
}

let product = reactive({ price: 10.quantity: 2 });
let salePrice = computed(() = > {
    return product.price * 0.9;
})
let total = computed(() = > {
    return salePrice.value * product.quantity;
})

console.log(total.value, salePrice.value);
product.quantity = 5;
console.log(total.value, salePrice.value);
product.price = 20;
console.log(total.value, salePrice.value);
Copy the code

Here we pass a function as a parameter to a computed method, build a REF object through the ref method in the computed method, and then pass the value returned by the getter method as the value returned by the computed method through the EFFCT method.

So we have a simple computed method that performs the same as before.

Five, source code learning advice

1. Build a reactivity. CJS. Js

This section describes how to package a Reactivity package from the Vue 3 repository to learn and use.

The preparation process is as follows:

  1. Download the latest Vue3 source code from Vue3 repository;
git clone https://github.com/vuejs/vue-next.git
Copy the code
  1. Install dependencies:
yarn install
Copy the code
  1. Build the Reactivity code:
yarn build reactivity
Copy the code
  1. Copy reactivity.js. Js to your learning demo directory:

The content built in the previous step will be stored in the Packages/Reactivity /dist directory. We just need to import the reactivity.cjs.js file from this directory in our learning demo.

  1. Learn to introduce:
const { reactive, computed, effect } = require("./reactivity.cjs.js");
Copy the code

Vue3 Reactivity file directory

In the packages/reactivity/ SRC directory of the source code, there are the following main files:

  1. Effect. ts: used to defineeffect / track / trigger ;
  2. BaseHandlers. Ts: Define Proxy handlers (get and set);
  3. Reactive. Ts: definitionreactiveMethod and create an ES6 Proxy;
  4. Ref. ts: Object accessor used by refs defining Reactive;
  5. Computed. Ts: Defines methods for calculating attributes;

(Image credit: Vue Mastery)

Six, summarized

This article takes you to learn how to implement the simple Vue3 response from scratch, implements the core methods in Vue3 Reactivity (effect/track/trigger/computed/REF, etc.), and helps you understand its core. Improve project development efficiency and code debugging ability.

Refer to the article

  • Vue Mastery

Phase to recommend

  1. Explore React composite events
  2. Explore the Vue. Js responsive principle
  3. Explore the system principle of Snabbdom module

My name is Ping an Wang, if my article is helpful to you, please like 👍🏻 to support me

My public number: front-end self-study class, every morning, enjoy a front-end excellent article. Welcome to join my front-end group to share and exchange technology, vx: Pingan8787.