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

After the above, it is time to turn to computed. Computed is very similar to reactive and REF, but the application scenarios are different and the principle is very similar. First, write one according to the ideas of REF

Draw the computed according to ref

Computed is actually an instance object of ComputedRefImpl, as implemented in the source code, so you can emulate the previous ref implementation as follows

const computed = value= > {
    return new ComputedRefImpl(value);
};

class ComputedRefImpl {
    constructor(value) {
        this._value = value;
    }

    get value() {
        track(this.'value');
        return this._value;
    }

    set value(newValue) {
        this._value = newValue;
        trigger(this.'value'); }}Copy the code

change

The parameters received by a computed function are not basic data, so you can’t get a value using this method, so change it below

Change parameters

When you use computed, you usually pass in a function as a getter, so you need to change the variable name in computed, as shown below

const computed = getter= > {
    return new ComputedRefImpl(getter);
};
Copy the code

Change the constructor

After handling computed functions as above, the constructor of the ComputedRefImpl needs to receive the getter, and we only need the getter for the value, not the setter, which also introduces two changes

  • The original value will exist directlythis._value, so we will need to executegetterTo get the value
  • forgetsetter

Handling getter

In fact, getter processing can be very simple, after all, only need to execute the getter to get the returned data, but then it loses the responsiveness, only when the manual use of XXX. Value value will execute the getter to calculate the value we need, which is obviously not what we want. It is therefore natural to think of using effect to listen on getters, as follows

class ComputedImpl {
    constructor(getter) {
        this.effect = effect(getter);
    }

    get value() {
        this._value = this.effect();
        track(this.'value');
        return this._value; }}Copy the code

As with ref, we actually access this._value, but we define a value accessor to perform a proxy-like operation on it, so we can access it through xxx.value. The difference from ref is that computed computations delegate value calculation to Effect without setters

The scheduling function

If you are observant, you can already see that this computed algorithm is not responsive because it only collects dependencies but does not trigger dependency updates, so you need a way to trigger dependency updates, which is done by scheduler scheduling functions in the source code. There is almost no room left in the above code to handle trigger, so you need to expand the effect

  1. toeffectDefine the second parameterscheduler
  2. Mount directly toeffectFnbody
const effect = (The scheduler = fnnull) = > {
    const effectFn = () = > {
        try {
            effectStack.push(effectFn);
            activeEffect = effectFn;
            return fn();
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1]; }}; effectFn();// Mount the scheduling function
    if(scheduler) { effectFn.scheduler = scheduler; }};Copy the code

The next step is to find a place to call the scheduler, which should be trigger

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

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

    deps.forEach(effectFn= > {
        // If there is a scheduling function, the scheduling function is executed first
        // Otherwise execute the side effect function itself
        effectFn.scheduler ? effectFn.scheduler(effectFn) : effectFn();      
    });
}
Copy the code

After the extension of effect is completed, of course, it is necessary to return to the main topic to trigger dependent update. The specific use is very obvious. Just pass in the scheduling function to Effect and trigger dependent update in the scheduling function

class ComputedRefImpl {
    constructor(getter) {
        this.effect = effect(getter, () = > {
            trigger(this.'value');
        });
    }

    get value() {
        this._value = this.effect();
        track(this.'value');
        return this._value; }}Copy the code

Computed caching mechanism

I’ve already done the basic attributes for computed data, but I’m sure you all know that a very important and core mechanism for computed data is caching

const a = ref(0);
const b = computed(() = > a.value + (Math.random().toFixed(2) - ' '));

const fn = effect(() = > {
    console.log(`b.value: ${b.value}`);
});

console.log('开始');

fn();
fn();

a.value = 10;
a.value = 20;

console.log('the end');
Copy the code

It looks messy, but what you actually do is very simple

  1. Define ref data A
  2. Defines computed data B that relies on A and is identified by a random number with two decimal digits reserved
  3. useeffectListen for the value of B
  4. Call effect twice
  5. Change the value of A twice

Look at the results

You can see that the random number is different each time, which means that every time effect is triggered, the fn in the example is called, b is recalculated, which is not necessary at all. We want it recalculated only when it depends on updates, which is a computed caching mechanism

What do you want to do

What we want to do with computed data is very simple, and that is to make it trigger a scheduler only when it depends on updates, so we can use a lock, and if it’s computed once now, we lock it, and the next time we call the getter, we don’t have to recalcate, and then we can open the lock when the scheduler fires, In the source code this lock is called _dirty

implementation

After the above analysis, we need a _dirty to indicate whether the current calculation has been done once, so as to avoid repeated calculations, as follows

class ComputedRefImpl {
    constructor(getter) {
        this._dirty = true;
        this.effect = effect(getter, () = > {
            if (!this._dirty) {
                // The lock is open
                this._dirty = true;
                trigger(this.'value'); }}); }get value() {
        if (this._dirty) {
            this._value = this.effect();
            / / locked
            this._dirty = false;
            track(this.'value');
        }
        return this._value; }}Copy the code

In the above code, after the getter is executed once, _dirty is set to false, so the next time the getter is executed the _dirty is not evaluated and returns directly. After the scheduler is triggered, _dirty is set to true and the getter is evaluated again. Then set it to false and repeat

How did the

Again, the example above

const a = ref(0);
const b = computed(() = > a.value + (Math.random().toFixed(2) - ' '));

const fn = effect(() = > {
    console.log(`b.value: ${b.value}`);
});

console.log('开始');

fn();
fn();

a.value = 10;
a.value = 20;

console.log('the end');
Copy the code

The results are as follows

This time, you can see that effect returns the same value because there is no calculation, but after changing the value of A triggers a dependency update, the calculation is repeated, so the following two values are different, enabling computed caching

To run the

Don’t run. It’s been run twice. It’s not a big problem

conclusion

The above is the basic implementation of computed, which is very rough and has not been implemented in many details. For example, computed in VUe3 supports passing in an object with getter and setter to obtain the properties of responsive computation, but this article has not implemented it, so I feel it is relatively simple. It’s mainly the caching mechanism that needs to be sorted out

Because the body has been changed this change that, more messy, in order to lower the complete code

// computed
const computed = getter= > {
    return new ComputedRefImpl(getter);
}

class ComputedRefImpl {
    constructor(getter) {
        this._dirty = true;
        this.effect = effect(getter, () = > {
            if (!this._dirty) {
                this._dirty = true;
                trigger(this.'value'); }}); }get value() {
        if (this._dirty) {
            this._value = this.effect();
            this._dirty = false;
            track(this.'value');
        }
        return this._value; }}Copy the code
// effect
let activeEffect;
const effectStack = [];

const effect = fn= > {
    const effectFn = () = > {
        try {
            effectStack.push(effectFn);
            activeEffect = effectFn;
            return fn();
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1]; }}; effectFn();return effectFn;
};
Copy the code
// track & trigger
const targetMap = new WeakMap(a);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);
}

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

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

    deps.forEach(effectFn= > {
        effectFn.scheduler ? effectFn.scheduler(effectFn) : effectFn();
    });
}
Copy the code