Writing in the front
Evaluate properties is a very useful API in Vue development that allows the user to customize a calculation method and then compute and return a new value based on some dependent reactive data. When the dependency changes, the calculated property is automatically recalculated to obtain the new value, which is easy to use. We see that evaluating properties is essentially evaluating dependencies, so why not just use functions? How are computed attributes implemented in Vue3?
Computed attribute computed
If the value of addone. value is changed, the value of addone. value cannot be changed. This is because:
- If what is passed to computed is a function, it is a getter function that can only get its value, not modify it directly
- In the getter function, the new value is recalculated from the reactive object, called the evaluated property. The reactive object is called the evaluated property dependency
const count = ref(1);
const addOne = computed(() = >count.value+1);
console.log(addOne.value);/ / 2
addOne.value++;//error
count.value++;
console.log(count.value);/ / 3
Copy the code
So, how do we change the addone.value value? That is, set the set function in computed and customize the modified values.
const count = ref(1);
const addOne = computed({
get:() = >count.value+1.set:val= >count.value=val-1
});
addOne.value = 1;
console.log(count.value);/ / 0
Copy the code
We study the source code:
export function computed<T> (
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// If function is passed in, it is read-only computed
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () = > {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
// Not a method specification is a custom getter setter
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true
let value: T
let computed: ComputedRef<T>
// Create effect, we know that passing lazy means that it does not execute immediately, computed means that computed upstream depends on changes, Trigger Runner effect is prioritized, and scheduler calls scheduler when it represents an effect trigger instead of calling effect directly
const runner = effect(getter, {
lazy: true.// mark effect as computed so that it gets priority during trigger
computed: true.scheduler: () = > {
// Set dirty to true when triggering updates
if(! dirty) { dirty =true
trigger(computed, TriggerOpTypes.SET, 'value')}}})// Construct a computed return
computed = {
__v_isRef: true.// expose effect so computed can be stopped
effect: runner,
get value() {
// When dirty is true, effect is executed to obtain the latest value
//
if (dirty) {
value = runner()
dirty = false
}
// If dirty is false, the value is not updated
track(computed, TrackOpTypes.GET, 'value')
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any
return computed
}
Copy the code
Computed attributes have two characteristics:
- Delayed computation: Only when we access the computed properties do we actually run computed getter function computation
- Cache: It caches the value of the last calculation internally and recalculates only when dirty is true. If dirty is false when accessing a calculated property, the value is returned
So, the advantage of evaluating a property is that as long as the dependency doesn’t change, you can use the cached value instead of executing a function to evaluate it every time the component is rendered.
As a small example of nested computation, we see that for addOne, the dependencies it collects are the component side effect rendering functions, and for count, the dependencies it collects are the runner functions inside addTwo. When we change count, we send notifications. We run setter functions in addOne, where dirty becomes true, and trigger sends notifications again. Then run the setter in addTwo, setting dirty to true; When we access the values in addTwo again and find that the dirty value is true, we perform addOne’s computed function first, and then count. Value + 1 in addOne’s computed function. That gives you the final printed value of 2.
const count = ref(0);
const addOne = computed(() = >{
return count.value + 1;/ / 1
})
const addTwo = computed(() = >{
return addOne.value + 1;/ / 2
})
console.log(addTwo.value);/ / 2
Copy the code
Thanks to the clever design of computed attributes, it works no matter how many layers are nested.
import {ref,computed} from "vue";
import {effect} from "@vue/reactivity";
const count = ref(0);
const addOne = computed(() = >{
return count.value + 1;
})
effect(() = >console.log(addOne.value+count.value))
function add(){
count.value++;
}
add();
Copy the code
We see the final output of the code above: 1, 3, 3
When we run addOne’s computed properties for the first time and count. Value is still 0, and addone. value is 1, we trigger and effect is executed, and it still prints 1. Then add() modifies the count.value value, triggering and executing the effect function because addOne is also a dependency on effect and runners are also a dependency on count.value. The modification of count.value executes the runners function, executes the addOne dependency again, and then triggers the effect function, so it prints 3 twice.
Objects returned by a computed function actually hijack the getters and setters for the value property, but why not access a computed property variable in a component’s template without manually adding a. Value?
Refer to the article
- Vue3 Core Source code Analysis
- Vue Chinese Community
- Vue3 Chinese Document
Write in the last
By understanding the working mechanism of computational attributes, we can understand the execution sequence of the code of the nested scenarios of computational attributes, know the two characteristics of computational attributes — delayed calculation and cache, and make reasonable use of computational attributes in the development of components.