preface

Vue3 beta version has been released for nearly two months, I believe that more or less everyone has learned some new features of VUE3, and some people are not moving to learn. In my opinion, technology must change constantly, new technology can improve productivity, and outdated technology must be eliminated. Five years ago you could get a decent job with a shuttle. Now few companies would ask for it. Just two days ago, The University of Utah also published an article about the production process of VUE3. If you are interested, you can click the link to view it. The article is in English, and those who are not good at English can read it with the help of the translation plug-in. Well, without further ado, the topic of this article is the responsiveness of handwriting VU3.

Example of vue3 code

Before writing the code, let’s take a look at how to use vue3. We can go to github.com/vuejs/vue-n… After you clone the code and use NPM install && NPM run dev, it will generate a package -> vue -> dist -> vue.global.js file so that we can use vue3. Create a new index.html file in the vue folder.


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Vue3 sample</title>
</head>
<body>
    <div id="app"></div>
    <button id="btn">button</button> 
    <script src="./dist/vue.global.js"></script>    
    <script>
        const { reactive, computed, watchEffect } = Vue;
        const app = document.querySelector('#app');
        const btn = document.querySelector('#btn');
        const year = new Date().getFullYear();
        let person = reactive({
            name: 'Fireworks Render farewell'.age: 23
        });
        let birthYear = computed((a)= > year - person.age);

        watchEffect((a)= > {
            app.innerHTML = I called ` < div >${person.name}This year,${person.age}Age, birth year is${birthYear.value}</div>`;
        });
        btn.addEventListener('click', () => {
            person.age += 1;
        });
    </script>
</body>
</html>
Copy the code

As you can see, clicking the button once at a time triggers Person. age += 1; Then watchEffect automatically executes and the calculated properties are updated accordingly, and now our goal is clear: Reactive, watchEffect, computed.

Reactive method

We know that VUe3 is responsive based on proxy. If you are not familiar with proxy, please refer to Teacher Ruan Yifeng’s ES6 tutorial: es6.ruanyifeng.com/#docs/proxy Reflect is also a new API provided by ES6. For details, please refer to the ES6 tutorial by Yifeng Ruan: es6.ruanyifeng.com/#docs/refle… In short, it provides a new API for manipulating objects, putting methods that belong to the language inside the Object into Reflect, and changing the old Object methods to return false. Let’s look at the code that proxies the get, set, and del operations on the object.

function isObject(target) {
    return typeof target === 'object'&& target ! = =null;
}

function reactive() {
    // Determine whether the object is a proxy
    if(! isObject(target)) {return target;
    }
    const baseHandler = {
        set(target, key, value, receiver) { // receiver: It always points to the object where the original read operation is located, usually the Proxy instance
            trigger(); // Triggers a view update
            return Reflect.set(target, key, value, receiver);
        },
        get(target, key, receiver) {
            return Reflect.get(target, key, value, receiver);
        },
        del(target, key) {
            return Reflect.deleteProperty(target, key); }};let observed = new Proxy(target, baseHandler);
    return observed;
}
Copy the code

Add update restrictions

This code looks fine, but when you add or remove elements from an array, you can listen for changes in the array itself, as well as changes in the length property, as shown in the following figure:

So we should only trigger an update when we add a new property, we added hasOwnProperty to compare the old value with the new value, and only update the view when we change the property of our own object or change the property of our own object and the value is different.

set(target, key, value, receiver) {
    const oldValue = target[key];
    if(! target.hasOwnProperty(key) || oldValue ! == value) {// Add attribute or set attribute old value does not equal new value
        trigger(target, key); // Trigger the view update function
    } 
    return Reflect.set(target, key, value, receiver);
}
Copy the code

Deep-level object listening

If the value of the object’s property is still the object, it has not been proxied. When we manipulate the object, set will not be triggered and the view will not be updated. The diagram below:

So how do we go about deep agency?

Let’s take a look at the operation person.hair.push(4). When we fetch Person.hair, we will call the Person get method to get the value of the property hair, so we can determine whether it is an object after it gets the value, and then conduct in-depth monitoring.

get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    return isObject(res) ? reactive(res) : res;
},
Copy the code

Cached proxied objects

Reactive resets the proxy when the propped object is executed. This should be avoided by caching the propped object using a HashMap, so that when the propped object is re-propped, the result can be returned.

  • Perform multiple proxy examples
let obj = {
    name: 'Fireworks Render farewell'.age: 23.hair: [1.2.3]}let person = reactive(obj);
person = reactive(obj);
person = reactive(obj);
Copy the code
  • definehashmapCaching proxy object

We use WeakMap to cache the proxy object, which is a weak reference object and does not cause a memory leak. Es6.ruanyifeng.com/#docs/set-m…

const toProxy = new WeakMap(a);// The object after the proxy
const toRaw = new WeakMap(a);// The object before the proxy

function reactive(target) {
    // Determine whether the object is a proxy
    if(! isObject(target)) {return target;
    }
    let proxy = toProxy.get(target); // The current object is in the proxy table
    if (proxy) { 
        return proxy;
    }
    if (toRaw.has(target)) { // The current object is proxied
        return target;
    }
    let observed = new Proxy(target, baseHandler);

    toProxy.set(target, observed);
    toRaw.set(observed, target);
    return observed;
}
let obj = {
    name: 'Fireworks Render farewell'.age: 23.hair: [1.2.3]}let person = reactive(obj);
person = reactive(obj); // The proxy returns the data retrieved from the cache
Copy the code

So the Reactive method is basically done.

Collect dependencies and update automatically

Let’s take a look at how we rendered the DOM earlier.

watchEffect((a)= > {
    app.innerHTML = I called ` < div >${person.name}This year,${person.age}Age, birth year is${birthYear.value}</div>`;
});
Copy the code

After the watchEffect function is initialized once by default, the DOM data is rendered, and the dependent data changes are automatically executed again, which automatically updates the content of the DOM. This is called collecting dependencies, reactive updates.

So where do we do dependency collection, and when do we notify dependency updates?

  • When we use data for presentation, it triggers the creationproxyThe object’sgetMethod, at which point we can collect dependencies.
  • It also triggers ours when the data changessetMethod, we are insetNotification dependency update in. This is actually a design pattern called publish and subscribe.

We collect dependencies in GET:

get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    track(target, key); // Collect dependencies and effect on the stack if the key on the target changes
    return isObject(res) ? reactive(res) : res;
}
Copy the code

Notifying dependency updates in a set:

set(target, key, value, receiver) { 
    if (target.hasOwnProperty(key)) {
        trigger(target, key); // Trigger the update
    }
    return Reflect.set(target, key, value, receiver);
}
Copy the code

You can see that we implemented a track method in GET to collect dependencies, and trigger triggered updates in set. Now that we know how it works, let’s see how to implement the watchEffect method.

WatchEffect method

The function we pass into the watchEffect method is the dependency we want to collect, and we store the collected dependency on a stack. A stack is an advanced data structure. Let’s look at the following code:

let effectStack = []; // Storage depends on data effect

function watchEffect(fn, options = {}) {
    // Create a responsive effect function, push an effect function to the effectsStack, execute fn
    const effect = createReactiveEffect(fn, options);
    return effect;
}

function createReactiveEffect(fn) {
    const effect = function() {
        if(! effectsStack.includes(effect)) {Effecy is already in the stack to prevent repeated addition
            try {
                effectsStack.push(effect); // Push the current effect onto the stack
                return fn(); / / execution fn
            } finally {
                effectsStack.pop(effect); // Avoid fn execution error, execute in finally, push current effect off stack
            }
        }
    }
    effect(); // Execute once by default
}
Copy the code

Associate effects with corresponding object properties

Above, we only collected FN and stored it in effectsStack, but we have not associated FN with the corresponding object properties. Next, we need to implement the track method to associate effect with the corresponding properties.

let targetsMap = new WeakMap(a);function track(target, key) { // If the key in taeget changes, the effect method on the stack is executed
    const effect = effectsStack[effectsStack.length - 1];
    // The latest effect is created before the association is created
    if (effect) {
        let depsMap = targetsMap.get(target);
        if(! depsMap) {// Set the matching value for the first rendering
            targetsMap.set(target, depsMap = new Map());
        }
        let deps = depsMap.get(key);
        if(! deps) {// Set the matching value for the first rendering
            depsMap.set(key, deps = new Set());
        }
        if(! deps.has(effect)) { deps.add(effect);// Add effect to the depsMap of the current targetsMap}}}function trigger(target, key, type) {
    // Trigger the update to find dependency effects
    let depsMap = targetsMap.get(target);
    if (depsMap) {
        let deps = depsMap.get(key);
        if (deps) {
            deps.forEach(effect= >{ effect(); }); }}}Copy the code

The data structure of targetsMap is relatively complex. It is a WeakMap object. The key of targetsMap is our target object. The value of the key corresponding to the Map object is a Set data structure that stores the effect dependencies corresponding to the current target.key. It might be clearer to look at the following code:

let person = reactive({
    name: 'Fireworks Render farewell'}); targetsMap = {person: {
        'name': [effect]
    }
}
/ / {
// target: {
// key: [dep1, dep2]
/ /}
// }
Copy the code

Execute the process

  • Collection process: ExecutionwatchEffectMethods,fnThat iseffectA push toeffectStackStack, executefnIf thefnUseful toreactiveObject that triggers the proxy objectgetMethod, while we aregetMethodtrackMethod to collect dependencies,trackThe method starts witheffectStackExtract the last one fromeffectThat’s what we just pushed onto the stackeffectAnd then determine whether it exists, and if it does, we start fromtargetMapExtract the correspondingtargetthedepsMapIf thedepsMapDoes not exist, we manually place the currenttargetAs akey.depsMap = new Map()Set as a value totargetMapAnd then we go fromdepsMapTo retrieve the current proxy objectkeyCorresponding dependencydepsIf it does not exist, store a new oneSetGo in and put the correspondingeffectAdded to thedepsIn the.
  • Update process: Modify the agent after the object, triggeredsetMethod, executiontriggerMethod passed intargetintargetsMapFound in thedepsMapThrough thekeyindepsMapTo find the correspondingdeps, loop execution inside the savedeffect.

The computed method

Before we write about computed, let’s review how it’s used:

let person = reactive({
    name: 'Fireworks Render farewell'.age: 23
});
let birthYear = computed((a)= > 2020 - person.age);
person.age += 1;
Copy the code

You can see that computed takes a function and then returns a processed value, and computed recalculates once the dependent data has been modified.

Actually computed it is also a watchEffect function, but it is a special one. In this case, we pass in two parameters, one for computed FN and the other for the watchEffect parameter {lazy: True, computed: true}, we didn’t handle these parameters when we wrote watchEffect earlier, so now we have to.

function computed(fn) {
    let computedValue;
    const computedEffect = watchEffect(fn, { 
        lazy: true.computed: true
    });
    return {
        effect: computedEffect,
        get value() {
            computedValue = computedEffect();
            trackChildRun(computedEffect);
            returncomputedValue; }}}function trackChildRun(childEffect) {
    if(! effectsStack.length)return;
    const effect = effectsStack[effectsStack.length - 1];
    for (let i = 0; i < childEffect.deps.length; i++) {
        const dep = childEffect.deps[i];

        if(! dep.has(effect)) { dep.add(effect); effect.deps.push(dep); }}}Copy the code

Modify the watchEffect method to accept an opstion parameter and add a lazy attribute judgment that does not execute the function passed in immediately when lazy is true, because computed methods do not execute immediately.

function watchEffect(fn, options = {}) {
    // Create a responsive effect function, push an effect function to the effectsStack, execute fn
    const effect = createReactiveEffect(fn, options);
    // start: added code
    if(! options.lazy) { effect() }// end: added code
    return effect;
}
Copy the code

Modify the createReactiveEffect method to add the options parameter, and add dePS to the current effect to collect the dependencies of the computed property, which in our example is the age property, and to save computed and lazy properties.

function createReactiveEffect(fn, options) {
    const effect = function() {
        // Determine if the effect is already in the stack to avoid repeating recursive loops, such as modifying dependent data in the listener function
        if(! effectsStack.includes(effect)) {try {
                effectsStack.push(effect); // Push the current effect onto the stack
                return fn(); / / execution fn
            } finally {
                effectsStack.pop(effect); // Avoid fn execution error, execute in finally, push current effect off stack}}}// start: added code
    effect.deps = [];
    effect.computed = options.computed;
    effect.lazy = options.lazy;
    // end: added code
    return effect;
}
Copy the code

Add the collected set of property dependencies to effect’s DEPS in the track method.

function track(target, key) { // If the key in taeget changes, the effect method on the stack is executed
    const effect = effectsStack[effectsStack.length - 1];
    // The latest effect is created before the association is created
    if (effect) {
        let depsMap = targetsMap.get(target);
        if(! depsMap) {// Set the matching value for the first rendering
            targetsMap.set(target, depsMap = new Map());
        }
        let deps = depsMap.get(key);
        if(! deps) {// Set the matching value for the first rendering
            depsMap.set(key, deps = new Set());
        }
        if(! deps.has(effect)) { deps.add(effect);// start: added code
            effect.deps.push(deps); // Mount the dependency set of properties to effect
            // end: added code}}}Copy the code

The trigger method uses the computed property previously saved in Effect to distinguish between a computed function and a normal function, and then saves them separately, and then executes the normal effect function first, then executes the computed function.

function trigger(target, key, type) {
    // Trigger the update to find dependency effects
    let depsMap = targetsMap.get(target);

    if (depsMap) {
        let effects = new Set(a);let computedRunners = new Set(a);let deps = depsMap.get(key);

        if (deps) {
            deps.forEach(effect= > {
                if (effect.computed) {
                    computedRunners.add(effect);
                } else{ effects.add(effect); }}); }if ((type === 'ADD' || type === 'DELETE') && Array.isArray(target)) {
            const iterationKey = 'length';
            const deps = depsMap.get(iterationKey);
            if (deps) {
                deps.forEach(effect= > {
                    effects.add(effect);
                });
            }
        }

        computedRunners.forEach(computed= > computed());
        effects.forEach(effect= >effect()); }}Copy the code

conclusioncomputedExecute the process

Let’s analyze the execution flow based on the following code.

const value = reactive({ count: 0 });
const cValue = computed((a)= > value.count + 1);
let dummy;

watchEffect((a)= > { 
    dummy = cValue.value;
    console.log(dummy)
});

value.count = 1;
Copy the code

Step 1: Turn the count object into a responsive object.

The second step: After executing computed, watchEffect is executed inside computed, and lazy and computed attributes are passed in. Since lazy is passed in as true, the generated effect is not executed immediately. For differentiation, this effect is collectively called computed effect. The fn passed in is called computed FN, that is, no data is added to the stack, and the cValue holds an object containing the computed effect and get methods.

Step 3: Execute the watchEffect method: This is the most critical step. Execute the watchEffect method, and since it has no lazy property, immediately execute the Effect method, add the current effect to the effectsStack, and execute fn.

Step 4: Perform fn, which gets the value of cValue and triggers a computed GET method, and perform effect saved in step 2.

Step 5: Compute effect, add compute Effect to effectsStack (effectsStack is [Normal Effect, compute effect]), and compute FN.

Step 5: Perform calculation of FN, which depends on the responsive object value. At this time, read the count attribute of value and trigger the GET method of value object, in which track method is executed to collect dependencies.

Step 6: Initialize targetsMap and depsMap. Then save the calculation effect to the dePS corresponding to count and dePS to the calculation effect dePS. Next step: In this way, a bidirectional collection relationship is formed. Counting effect saves all the dependencies of count, and count also stores the dependencies of counting effect. After performing the next step of track method, the obtained value of value.count is returned and stored in computedValue. And then we go ahead and execute.

Step 6: When we run trackChildRun and calculate fn, we push effect off the stack. At this point, the top of the stack is normal effect. First, we get the bottom element of the stack, which is the rest of the normal effect. When we executed track in the previous step, we saved the dependency set corresponding to the count attribute in the DEPS of calculating effect. At this time, there is only one element in the DEPS [calculating effect]. Now add the general effect to the DEP. So depsMap is {count: [calculate effect, normal effect]}.

Step 7: Execute value.count = 1, trigger the set method, execute trigger method, and obtain the DEPS corresponding to count, namely [calculation effect, general effect]. Then compute effect and normal effect are executed.

Thank you

Thank you for reading this article and giving it a thumbs up before you go. (^o^)/~