preface

It feels like time is so fast, it has been more than a month without more text. This picture is from 2020

Starting a journey to 2021, this article is first published in 2021… Hope readers dig friends more support ha ~

After sorting out the Vue3 I have learned in these days, I think Vue3 will become a trend. Then hurry up to learn ~

setup

The timing of this API call: create the component instance, initialize the props, and then call the setup function. From the perspective of the lifecycle hook, it is called before the beforeCreate hook.

We can write most of the business logic in this function. In Vue2 we separate out the code logic in the form of each option, such as methods, data, and watch options. Vue3 has now changed this mode (ps: not to say changed, because it is also compatible with Vue2).

Isn’t that the theory of The Three Kingdoms? Long apart, long together, long apart

Setup has two optional parameters.

  • Props — property (a responsive object that can be listened to (watch))
  • Context object – with this object you don’t have to worry about where this points to anymore

Generate reactive objects

How to understand reactive objects? I used examples to describe a scenario in another article, see: [Advanced vue.js] Summarize what I learned from the Vue source code.

Now, how does Vue3 generate responsive data

reactive

This function takes an object as an argument and returns a proxy object. The reactive function generates objects that lose their responsiveness if they are not properly used.

Let’s first look at what happens when we lose responsiveness:

setup(){
    const obj=reactive({
      name:'golden'.age:10
    });
    return{
      ...obj, // The response is lost because obj has lost its reference}},Copy the code

How to solve this problem of unresponsiveness?

return {
	 obj, {{age}} {age}}. toRefs(obj){{age}} {age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, age, etc.
}
Copy the code

As for what toRefs is, we’ll talk about it later, but for now it’s a solution to the problem of losing responsiveness.

ref

Because the reactive function can represent an object, but not the basic data type, you need to use the ref function to indirectly process the basic data type. This function packs the basic data type into a reactive object that can track changes.

 <span @click="addN">Click on me</span>
 <span>{{n}}</span> 
Copy the code
setup(){
    
    const n=ref(1); // The generated n is an object so that vue can monitor it
    function addN(){
      console.log(n.value,n,'... ')
      n.value++;  N is the object and value is its value
    }
    return {
      n,      {{n}} does not require.value
      addN,
    }
 }
Copy the code

How do I implement the REF?

Before implementing ref, let’s take a look at the TRACK and Trigger apis provided in the Vue3 source code.

Tracks and triggers are at the heart of dependency collection

Track is used to track collection dependencies: three parameters are received. The trigger is used to trigger the response (perform the effect). (Ps: Later in this article, I will explain how to implement both apis, but for now I will leave questions…)

In this case, js is single-threaded, so you can intercept the dependency collection when getting the value, and trigger the dependency update when setting the update value. Therefore, the implementation of the ref can be roughly written as:

function myRef(val: any) {
  let value = val
  const ref = {
    get value() {
      // Collect dependencies
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal: any) {
      if(newVal ! == value) { value = newVal// Trigger the response
        trigger(r, TriggerOpTypes.SET, 'value')}}}return ref
}
Copy the code

ToRef and toRefs

If the generated object is wrapped in toRefs in the contents of the reactive module, the generated object points to the refs of the object’s property.

Use 🌰 on the Vue3 website to further understand:

const state = reactive({
  foo: 1.bar: 2
})

const stateAsRefs = toRefs(state)
// ref and raw property "link"
state.foo++
console.log(stateAsRefs.foo.value) / / 2

stateAsRefs.foo.value++
console.log(state.foo) / / 3
Copy the code

ToRef and toRefs are implemented in the same way. ToRef is used to convert a key value of a responsive object toRef. The toRefs function converts all keys of a reactive object toRefs.

How to implement toRef

Since the target object itself is reactive data that has already gone through dependency collection, the response triggers this interception, so it is not needed when implementing toRef.

If toRef is implemented here, then toRefs will come naturally… (ps: through traversal can be obtained)

function toRef(target, key) {
    return {
        get value() {
            return target[key]
        },
        set value(newVal){
            target[key] = newVal
        }
    }
}
Copy the code

thinking

When using the ref related content, we see that there is no.value in the template to get the data, but there is.value in the JS code block to access the properties.

This can be summarized as Vue3’s automatic unpacking:

JS: You need to access the wrapper object through.value

Template: Automatically unboxing, that is, the template does not need to use. Value access

Side effects

There is a concept of side effects in Vue3, so what are side effects?

The following API is related to this side effect!

effect & watchEffect

The effect function is used to define side effects. Its argument is the side effect function, which may cause side effects. By default, this side effect is executed first.

How to understand this side effect?

You can tell by the following code:

import { effect,reactive } from '@vue/reactivity';
// import {watchEffect} from '@vue/runtime-core';
// Use reactive() function to define reactive data
const obj = reactive({ text: 'hello' })
// Use effect() to define the side effect function
effect(() = > {
     document.body.innerText = obj.text
})

// watchEffect(() => {
// document.body.innerText = obj.text
// })

// Modify the reactive data after one second, which triggers the side effect function to re-execute
setTimeout(() = > {
  obj.text += ' world'
}, 1000)
Copy the code

The cb callback function that effect receives is a side effect that is triggered when the data changes.

thinking

import {effect,reactive } from '@vue/reactivity';
const obj = reactive({ a: 1 })
effect(() = > {
   console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
// result: 2,3,4
Copy the code

When the effect function is associated with the responsive data, the effect callback is executed whenever the responsive data changes. That is to say, change several times to execute several times. Is this bad performance??

Effect can be passed a second argument {scheduler: XXX}, specifying the scheduler: XXX.

The scheduler specifies how to run the side effect function.

The watchEffect function is based on this scheduler to optimize the implementation of side effects.

import {reactive } from '@vue/reactivity';
 import {watchEffect} from '@vue/runtime-core';
const obj = reactive({ a: 1 })
watchEffect(() = > {
   console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
// Result: 4
Copy the code

So what’s the idea? The cb is collected in a queue, and the CB is already in the queue. Then execute the CB in the queue through the while loop. Pseudo code:

const queue=  [];
let dirty = false;
function queueHandle(job) {
  if(! queue.includes(job)) queue.push(job)if(! dirty) { dirty =true
    Promise.resolve().then(() = > {
      let fn;
      while(fn = queue.shift()) { fn(); }}}})Copy the code

Use watchEffect instead of effect in a development environment.

Asynchronous side effect

What I just described above is that in the case of synchronization, the side effects of asynchrony, such as another Ajax request, occur when the data changes. We can’t tell which request is faster, which creates uncertainty…

So how do you resolve this uncertainty?

Looking for ways to clean up callbacks when they fail, I’m thinking of invalidating the pending asynchronous operation by clearing up the last asynchronous side effect while executing this one side effect.

Vue3 accepts an onInvalidate function as an input in the watchEffect callback.

A rough principle can be achieved based on effect:

import { effect } from '@vue/reactivity'

function watchEffect(fn: (onInvalidate: (fn: () => void) = >void) = >void) {
  let cleanup: Function
  function onInvalidate(fn: Function) {
    cleanup = fn
  }
  // Encapsulate effect
  // Before executing the side effect function, nullify the previous null function
  effect(() = > {
    cleanup && cleanup();
    fn(onInvalidate)
  })
}
Copy the code

How to stop side effects

Vue3 provides a stop function to stop side effects.

The effect function returns a value, which is the effect itself. Pass this return value to the stop function, and the subsequent changes to the data will not implement the effect callback function is called.

The difference between

WatchEffect maintains a relationship with the component instance and the component state (whether it was uninstalled, etc.). If a component is uninstalled, watchEffect is stopped, but Effect is not.

Effect is a side effect that needs to be understood, otherwise it will not be removed voluntarily.

watch

Let’s talk about watch, which acts as a listener for the component. Watch needs to listen to a specific data source and execute side effects in the callback function. By default, it is also lazy, meaning that the callback is performed only when the source being listened on has changed.

The implementation of this API is not very different from Vue2, the key Vue3 is to extend the functionality of Vue2, which is more perfect than Vue2…

// Specific responder object listening
// Enable immediate: true. This is the same as 2.0
watch(
  text,
  () = > {
    console.log("watch text:"); });// A specific responsive object listener can retrieve old and new values
watch(
  text,
 (newVal, oldVal) = > {
    console.log("watch text:", newVal, oldVal); });// Multi-responsive object listening
watch(
  [firstName,lastName],
 ([newFirst,newLast], [oldFirst,oldlast]) = > {
   console.log (newFirst,'New first value',newLast,'New last value')});Copy the code

In contrast to watchEffect, Watch allows us to:

  • Lazy execution side effects;
  • To be more specific about what state should trigger the listener to restart;
  • Access values before and after a change in listening state.

triggerRef

Remember Vue2’s $forceUpdate for mandatory refreshes? There is also an API in Vue3 that forces side effects to be triggered. Let’s start with the following code:

//shallowRef only represents the ref object itself. That is, only.value is represented. The object referred to by.value is not represented
const shallow = shallowRef({
  greet: 'Hello, world'
})

// Record "Hello, world" the first time you run it
watchEffect(() = > {
  console.log(shallow.value.greet)
})

// This will not trigger because the ref is very shallow
shallow.value.greet = 'Hello, universe'

// Manually trigger, record "Hello, universe"
triggerRef(shallow)

Copy the code

The life cycle thing

2. Comparison between x and 3.0

BeforeCreate -> Use setup() created -> Use setup() beforeMount -> onBeforeMount -- Use mounted -> onMounted only in setup -- only use beforeUpdate in setup -- > onBeforeUpdate in setup -- only use updated in setup -- > onUpdated in setup -- Only use beforeDestroy in setup -- > OnBeforeUnmount -- Can only use Destroyed -> onUnmounted in setup -- errorCaptured -> onErrorCaptured in Setup -- Can only be used in setupCopy the code

Get the real Dom element

$refs.XXX. Vue3 also obtains the real DOM element from ref, but the writing is changed.

 <div v-for="item in list" :ref="setItemRef"></div>
  <p ref="content"></p>
Copy the code
import { ref, onBeforeUpdate, onUpdated } from 'vue'

export default {
setup() {
	// Define a variable to receive the DOM
  let itemRefs = [];
  let content=ref(null);
  const setItemRef = el= > {
    itemRefs.push(el)
  }
  onBeforeUpdate(() = > {
    itemRefs = []
  })
  onUpdated(() = > {
    console.log(itemRefs)
  })
  // The return name should be the same as the DOM ref, so that the DOM callback is received
  return {
    itemRefs,
    setItemRef,
    content
  }
}
}
Copy the code

Provide/inject — develop plug-ins

Previously, when developing a common component or encapsulating a common plug-in, you would hook functionality on a prototype or use mixins.

This is not a good idea. Hanging on to the prototype makes Vue look bloated and there is also the possibility of name conflicts, mixins making code jump and readers’ logic jump.

Now there is a new solution, the Commotion API, to implement plug-in development

1. The public functions of the plug-in can be wrapped with the provide function and inject function.

import {provide, inject} from 'vue';
// Use symbol to create variable name conflicts, and give the user naming rights
const StoreSymbol = Symbol(a)export function provideString(store){
  provide(StoreSymbol, store)  //
}

// The target plug-in
export function useString() {
  const store = inject(StoreSymbol)  
  /* * * * * * * * * * * *
  return store
}
Copy the code

2. Initialize data in the root component, introduce provideString function, and transmit data

export default {
  setup(){
    // Some initial 'configuration/operations' can be done here
    // It needs to be placed on the corresponding root node, because provide and inject are dependent
     provideString({
       a:'Maybe I'm Axios.'.b:'Maybe I'm a message popover.'}}})Copy the code

3. Introduce plug-ins into desired components to complete the use of related functions

import { useString } from '.. / plug-in ';

export default {
  setup(){
    const store = useString(); // The plugin is ready to use}}Copy the code

Vue3 responsive principle

Responsive disadvantages of VUE2:

  • The default is recursion
  • Array length changes in response are not supported
  • Properties that do not exist on an object are not intercepted

How to implement

The remaining questions above are now implemented here.

Let’s first analyze how the Vue3 responsive principle is implemented:

  • Vue3 is not in useObject.definePropertyIntercept. Instead, the alternative is ES6Proxy.
  • Dependency collection does not passDepClasses andWatchClass, but throughtrackFunction to associate a target object with a side effect bytriggerMake the dependent response.
  • Side effects are stored in the form of a stack, advanced after the idea.

This is just the general idea, the implementation process there are a lot of details of the analysis, the next step by step to explain…

The first step:

To implement a reactivity, consider the following questions:

{a:{b:2}} {a:{b:2}} {a:{b:2}}

2. What happens when an object calls the reactivity function multiple times?

3. What if an object’s proxy object calls the reactivity function?

4. How to determine whether the object is a new attribute or a modified attribute?

// Utility class functions
function isObject(obj){
  return typeof obj==='object'&& obj! = =null?true:false;
}
function isOwnKey(target,key){
  return Object.hasOwnProperty(target,key)?true:false;
}

Copy the code
function reactivity(target){
 return  createReactivity(target);
}
let toProxy=new WeakMap(a);// Used to connect the target object (key) with the proxy object (value)
let toRaw=new WeakMap(a);// Used to connect the proxy object (key) with the target object (value)

function createReactivity(target){
  if(! isObject(target)){return ;
  }
  let mapProxy=toProxy.get(target); // Handle the case where the target object has been reactive multiple times
  if(mapProxy){
    return mapProxy;
  }
  if(toRaw.has(target)){  Let proxy=reactivity (obj); // Let proxy=reactivity (obj); Reactivity (proxy);
    return target;
  }
  let proxy=new Proxy(target,{
    get(target,key,receiver){
      let res=Reflect.get(target,key);
      // Collect dependencies when retrieving attributes
      track(target,key);  ++++
      returnisObject(res)? reactivity(res):res;// Recursively implement multiple layers of nested objects
    },
    set(target,key,value,receiver){
      let res=Reflect.set(target, name, value,receiver);  
      let oldValue=target[key];  // Get the old value, which is used to compare the new value with the old value
      
      /** Add a new attribute or modify a new attribute by determining whether the key already exists. Adding a new attribute may change the old attribute, which is not considered by most people */
      if(! isOwnKey(target,key)){console.log('New attribute');
        // Publish when the property is set
        trigger(target,'add',key); + + +}else if(oldValue! ==value){console.log('Modify properties');
        trigger(target,'set',key); + + +}return res;
    },
  })
  toProxy.set(target,proxy);
  toRaw.set(proxy,target);
  return proxy;
}
Copy the code
The second step:

Now to implement a side effect function, the simplest way to consider this function is to pass in a fn as an input.

We store the effect in a global queue, and the queue is stored as I said before in a stack queue.

So effect will be executed once by default at the beginning, so collect effect first, and then use js single thread principle to correlate the target object with effect.

let effectStack=[];  // Stack queue, first in, last out

function effect(fn){
  let effect=createEffect(fn);
  effect();  // The default is to execute first
}
function createEffect(){
  let effect=function(){
    run(effect,fn);
  }
  return effect;
}
1. Collect effect,2. Execute fn
function run(effect,fn){
  // Use try-finally to prevent finally code from being executed when an error occurs
  // Using js is single-threaded. Collect before you associate
  try{
    effectStack.push(effect);
    fn();
  }finally{ effectStack.pop(); }}Copy the code
Step 3:

The key to this step is how to correlate the key and effect of the target object while collecting dependencies. Here’s a special data structure:

 {
   target: {key1:[effect1,effect12],
     key2:[effect3,effect4],
     key3:[effect5],
   }
 }
Copy the code

Each target object (as a key) has a corresponding value (which is an object), which in turn maps the key and effect.

So in response to dependencies, since we have obtained the relationship between effect and the key corresponding to the target object, the traversal triggers.

let targetMap=new WeakMap(a);// Collect dependencies
function track(target,key){
  let effect=effectStack[effectStack.length-1]; // Retrieve effect from stack to see if there are any side effects
  if(effect){  // Create dependencies for relationships
    let depMap=targetMap.get(target);
    if(! mapRarget){ targetMap.set(target,(depMap=new Map()));
    }
    let deps=depMap.get(key);
    if(! deps){ mapDep.set(key,(deps=new Set()));
    }
    if(! deps.has(effect)){ deps.add(effect) } } }// Response dependency
function trigger(target,type,key){
  let depMap=targetMap.get(target);
  if(depMap){
   let deps= depMap.get(key);  // The effect corresponding to the current key
   if(deps){
     deps.forEach(effect= >effect()); }}}Copy the code

At this point, the responsive principle of Vue3 is basically implemented, of course you can read the source code after this article, I believe you will understand the source code more easily ~

conclusion

In the process of learning, turned over Vue3 source code to see 3 days, to tell the truth is really a headache. Instead of looking at the source code, I decided to start by learning how to use Vue3, then figure out why, and finally how to implement it.

The road to knowledge is a long one, but high and low I will search

The resources

Vue3 Chinese documents

[Vue3 official tutorial] 🎄 word notes | synchronous learning video guide