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 directly
this._value
, so we will need to executegetter
To get the value - forget
setter
吧
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
- to
effect
Define the second parameterscheduler
- Mount directly to
effectFn
body
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
- Define ref data A
- Defines computed data B that relies on A and is identified by a random number with two decimal digits reserved
- use
effect
Listen for the value of B - Call effect twice
- 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