In this paper, the corresponding source location vue – next/packages/reactivity/SRC/effect. The ts line 53

Front knowledge

Vue3 source code uses WeakMap to store the corresponding relationship between responsive objects and dependencies, while using WeakMap is completely out of performance consideration, so Map can be used instead. WeakMap’s key value must be an object, which I think directly leads to Reactive being used in everyday use only to encapsulate object data, unlike REF (later implemented).

Write to see

You can look at the results first, implement the prototype, and then refine the details as shown below

const reactive = target= > {
    return new Proxy(target, {
        get(target, key, receiver) {
            track();
            const res = Reflect.get(target, key, receiver);
            return res;
        },

        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver);
            trigger();
            returnres; }}); };const effect = () = > {
    / /???????
};

const track = () = > {
    / /???????
};

const trigger = () = > {
    / /???????
};

// Run and see
const a = reactive({
    count: 0}); effect(() = > {
    console.log(`effect called...... a.count:${a.count}`); // effect called...... A. mount: 0 uses effect once by itself
});

a.count = 10; // effect called...... a.count: 10
a.count = 20; // effect called...... a.count: 20
Copy the code

After analyzing the output results, the following conclusions can be preliminarily drawn

  1. effectThe function receives a callback function, temporarily calledeffectFn
  2. calleffectAnd then it will do it itself first
  3. Each update or value operation on a reactive object is called againeffectFn

So it’s pretty clear what Effect is basically trying to do

const effect = effectFn= > {
    effectFn();
};
Copy the code

Yeah, it’s just a simple one, with a little bit of detail

track

Next to track, dependency collection essentially just saves the dependency relationship of the target object in an appropriate data structure (named targetMap below). In simple terms, it saves the mapping relationship between different target objects and effectFn, while WeakMap is used to save it in the source code. In line with the principle of running, Map is used to achieve this. In targetMap, you need to store three types of data: the target object, the key value, and the corresponding effectFn. Therefore, you need to nest a Map. The effectFn cannot be repeated, and there may be many different effectFn

{
    [targetMap]: {
        [key]: [effectFn] // [effectFn] is a Set}}Copy the code

So all we have to do is store the effectFn and the target and the key step by step.

Once the data structure is resolved, a new problem is how to expose effectFn in effect. Sharing data between functions at the same level is not so much a fancy operation. We will define a global variable activeEffect to hold the effectFn currently being executed, assuming that activeEffect is already defined

After the analysis, the concrete implementation of track is as follows

const targetMap = new Map(a)const track = (target, key) = > {
    if(! activeEffect) {return;
    }

    let depsMap = targetMap.get(target);
    if(! depsMap) { depsMap =new Map(a); targetMap.set(target, depsMap); }let deps = depsMap.get(key);
    if(! deps) { deps =new Set(a); depsMap.set(key, deps); }// Save the effect function that is currently executing
    deps.add(activeEffect);
};
Copy the code

trigger

The rest of the trigger is very simple. You just need to extract the corresponding DEPS and execute the effectFn

const trigger = (target, key) = > {
    const depsMap = targetMap.get(target);
    if(! depsMap) {return;
    }

    const deps = depsMap.get(key);
    if(! deps) {return;
    }

    // Execute all effectFn's previously stored
    deps.forEach(effectFn= > effectFn());
};
Copy the code

effect

Returning to the effect method, effect requires:

  1. Execute the function passed infn
  2. Expose what is currently being executedeffectFn

The above process is encapsulated in an internal function, as follows

let activeEffect;
const effect = fn= > {
    const effectFn = () = > {
        activeEffect = fn;
        return fn();
    };

    // Execute once
    effectFn();

    return effectFn;
};
Copy the code

Ran to see

The above has basically completed the preparation of effect. Pull the reactive I wrote last time to do a small test

const a = reactive({
    count: 0}); effect(() = > {
    console.log(`effect called...... a.count:${a.count}`); // effect called...... A. mount: 0 calls effect once by itself
});

a.count = 10; // effect called...... a.count: 10
a.count = 20; // effect called...... a.count: 20
Copy the code

Small refinements

This has roughly completed the effect section, but there is still a problem. Let’s take a look at what the problem looks like

const a = reactive({ num1: 10.num2: 20 });

effect(() = > {
    effect(() = > {
        console.log(`a.num2: ${a.num2}`);
    });
    console.log(`a.num1: ${a.num1}`);
});

a.num1 = 100;
a.num1 = 200;
a.num1 = 300;
Copy the code

Here are the results

This is obviously not normal. The first and second are the first implementations of the initialization, which is fine, but when I assign a.num1, I print a.num2

Why did it go wrong

In fact, it is not hard to imagine that we use an activeEffect to record the effectFn currently executed. When an effect is nested within an effect, the inner effect overwrites the outer effect. Therefore, an outer effect is triggered, but an inner effect is executed. How to solve this problem

Since the coverage will be, you only need to use something to the record and control the execution order, here are likely to conjure up function execution stack, such is the execution stack, when recursive produce pressed new function stack, after playing stack, in order to control the order of the function, so we can use a stack to auxiliary effectFn, Control it to execute the inner effect first, and then execute the outer effect

change

The above analysis of the problem and the solution, it is very simple to implement, the following directly gives the modified code

const effectStack = [];
let activeEffect;

const effect = fn= > {
    const effectFn = () = > {
        try {
            effectStack.push(effectFn);
            // The assignment must be done once, because return is required below
            activeEffect = effectFn;
            return fn();
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1]; }}; effectFn();return effectFn;
};
Copy the code

The idea is to use a stack to control activeEffect. Each time effectFn is executed, it is pushed onto the stack. ActiveEffect is always the top element of the stack, and when it is executed, it pops onto the stack. ActiveEffect overrides are addressed

conclusion

The core part of effect is that effect exposes the effectFn currently being executed, and track maintains a targetMap to store the mapping between target and effectFn for trigger consumption

Q&A

Q: Is it that simple? A: Of course not. In the source code, effect is implemented through a ReactiveEffect class, which internally implements the member methods run and stop. However, it is not very critical (in fact, it is very critical, but I write it this way also can barely achieve the purpose), so only the simplest implementation, to see the source code can be more specific. The path and file name are at the beginning of the article, down to the line number

Q: Is it a problem for you to write so simple? A: I don’t know. There will be some problems if many special cases are not handled. But these are the most important ones

Q: Who can understand your text in such a mess? A: Here’s the complete code

// effect
const effectStack = [];
let activeEffect;

const effect = fn= > {
    const effectFn = () = > {
        try {
            effectStack.push(effectFn);
            activeEffect = effectFn;
            return fn();
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1]; }}; effectFn();return effectFn;
};

const targetMap = new WeakMap(a);// track
function track(target, key) {
    if(! activeEffect)return;
    

    let depsMap = targetMap.get(target);
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
    }

    let deps = depsMap.get(key);
    if(! deps) { depsMap.set(key, (deps =new Set()));
    }

    deps.add(activeEffect);
}

// trigger
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if(! depsMap)return;

    const deps = depsMap.get(key);
    if(! deps)return;

    deps.forEach(effectFn= > effectFn());
}
Copy the code