preamble
Vue 3.5 responsive refactoring is divided into two main parts:bidirectional linked list
cap (a poem)version count
. In the previous post we talked aboutbidirectional linked list In this article, we'll move on toversion count
。
Ouyang is also graduating at the end of the year, join Ouyang's interview exchange group (to share information on internal promotion), high-quality vue source code exchange group
version count
Before reading this article it is best to read what Ouyang wrote earlierbidirectional linked list article, otherwise some parts might look rather confusing.
in the previous chapterbidirectional linked list In the article we learned that there are three main sections in the new responsive model:Sub subscribers
、Dep dependency
、Link node
。
-
Sub subscribers
: The main ones are watchEffect, watch, render function, computed, and so on. -
Dep dependency
: The main responsive variables are ref, reactive and computed. -
Link node
: ConnectionsSub subscribers
cap (a poem)Dep dependency
The Bridge Between.Sub subscribers
would like to visitDep dependency
passLink node
as wellDep dependency
would like to visitSub subscribers
It can only be done byLink node
。
Careful peeps may have noticed that the computed computation property is not only about theSub subscribers
neverthelessDep dependency
。
The reason for this is that computed can be used likewatchEffect
That listens to the responsive variables inside and triggers the computed callback when the responsive variable changes.
It is also possible to use the return value of computed as a normal responsive variable like ref.That's why we say that computed is not only Sub subscriber or Dep dependent.
version countwhich is implemented by four versions, namely: the global variableglobalVersion
、、
cap (a poem)
。
-
globalVersion
is a global variable with an initial value of0
, triggered only when a responsive variable is changedglobalVersion++
。 -
Yes, it is.
dep
Depends on an attribute above with an initial value of 0. When dep depends on a normal responsive variable like ref, only the responsive variable is triggered by a change in the++
. When the computed computed property is used as a dep dependency, it is only triggered when the final computed value of computed is changed++
。 -
is a property on top of the Link node with an initial value of 0. After each responsive update it will remain the same as the
has the same value. Before responsive updating is when you pass the
cap (a poem)
is the same to determine if an update is needed.
-
: Calculate the version above the property if
=== globalVersion
It means that there are no responsive variables to change, and the callbacks to compute the attributes don't need to be re-executed.
The biggest beneficiary of version counting is the computed computed property, which we'll use as an example in the next part of this post.
Look at an example.
Let's look at a simple demo with the following code:
<template>
<p>{{ doubleCount }}</p>
<button @click="flag = !flag">switch modes or data streamsflag</button>
<button @click="count1++">count1++</button>
<button @click="count2++">count2++</button>
</template>
<script setup>
import { computed, ref } from "vue";
const count1 = ref(1);
const count2 = ref(10);
const flag = ref(true);
const doubleCount = computed(() => {
("computed");
if () {
return * 2;
} else {
return * 2;
}
});
</script>
In computed according to theto determine whether to return the value of
* 2
nevertheless * 2
。
The question then arises whenflag
The value of thetrue
When clicking on thecount2++
button.("computed")
Will it perform a print? That is.doubleCount
value will be recalculated?
The answer is:will not (act, happen etc). Althoughcount2
is also a responsive variable used in computed, but he is not involved in the computation of the return value, so changing him will not cause computed to recalculate.
Some students want to ask how it is possible to achieve such fine control. This is thanks to theversion count
up, and we'll talk about it in detail next.
dependent trigger
It's the same demo as before, initializing theflag
value is true, so in computed there will be a change to thecount1
variable performs a read operation and then triggers a get intercept.count1
This ref responsive variable is the one used by theRefImpl
Class new out of an object, the code is as follows:
class RefImpl {
dep: Dep = new Dep();
get value() {
()
}
set value() {
();
}
}
In a get intercept it will execute the()
whichdep
attributableDep
The code for an object of class new is as follows
class Dep {
version = 0;
track() {
let link = new Link(activeSub, this);
// ...an omission
}
trigger() {
++;
globalVersion++;
();
}
}
existtrack
method using theLink
class new out of a link object.Link
The class code is as follows:
class Link {
version: number
/**
* Pointers for doubly-linked lists
*/
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
constructor(
public sub: Subscriber,
public dep: Dep,
) {
=
=
=
=
=
=
undefined
}
}
Here we will focus only on the Link in theversion
attribute, the other attributes were covered in the previous article on bidirectional linked lists.
existconstructor
hit the nail on the headdo sth (for sb)
Assigning a value ensures that the
cap (a poem)
values are equal, that is, they are equal to 0. Because the
which has an initial value of 0, will be covered next.
When we click on thecount1++
button will make the responsive variablecount1
values are self-increasing. Since thecount1
is a ref responsive variable, so it triggers its set intercept. The code is as follows:
class RefImpl {
dep: Dep = new Dep();
get value() {
()
}
set value() {
();
}
}
The execution in set intercept is()
,trigger
The function code is as follows:
class Dep {
version = 0;
track() {
let link = new Link(activeSub, this);
// ...an omission
}
trigger() {
++;
globalVersion++;
();
}
}
I've told you before.globalVersion
is a global variable with an initial value of 0.
Dep aboveversion
The initial value of the attribute is also 0.
existtrigger
were executed separately in the++
cap (a poem)globalVersion++
This is the dep to which this is pointing after the execution ofcap (a poem)
globalVersion
value is now 1. And at this pointis still 0, this time
cap (a poem)
values would already be unequal.
Then it's time to executenotify
method notifies subscribers of updates according to the new responsive model, which at this point in our example is shown below:
If a modified responsive variable triggers multiple subscribers, such as thecount1
variable is used by more than onewatchEffect
Use, modifycount1
The value of the variable would then need to trigger updates from multiple subscribers.notify
method is to put multiple update operations into a single batch to improve performance. Due to space constraints, we won't go into the details of thenotify
method, you only need to know the contents of the method that executes thenotify
method then triggers an update from the subscriber.
(These two paragraphs arenotify
(the logic within the method) follows the normal logic if thecount1
A change in the value of a variable can be made with theLink2
Node FindingSub1
subscriber, and then executes the subscriber'snotify
method and thus update it.
If ourSub1
The subscriber is the render function, is this normal logic. But at this point ourSub1
Subscribers are computing propertiesdoubleCount
Instead of directly executing the callback function of the computed attribute, if the subscriber is a computed attribute, it will go directly to notify the subscriber of the computed attribute to update it, and it will only go to execute the callback function of the computed attribute before updating it (which will be talked about in the next article). The code is as follows:
if (()) {
// if notify() returns `true`, this is a computed. Also call notify
// on its dep - it's called here instead of inside computed's notify
// in order to reduce call stack depth.
()
}
()
The result of true for the computed attribute means that the current subscriber is the computed attribute, and then the corresponding subscriber will be triggered when the computed attribute "acts as a dependency". Here we have the computed attributedoubleCount
is used in the template, so the computed attributedoubleCount
of the subscriber is the render function.
So here is the call to()
Calculated properties will not be triggereddoubleCount
Instead of re-executing the callback function, it goes and triggers the computation of the propertydoubleCount
of the subscriber, which is the render function. Before executing the render function it will go back through thedirty examination(which relies on version counting) to determine if it needs to re-execute the callback to compute the attributes, and if it does then go ahead and execute the render function to re-render.
dirty examination
allSub subscribers
The internals are all based onReactiveEffect
class to implement it, calling the subscriber'snotify
method to notify the update is actually underlying the call to theReactiveEffect
classrunIfDirty
method. The code is as follows:
class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
/**
* @internal
*/
runIfDirty(): void {
if (isDirty(this)) {
();
}
}
}
existrunIfDirty
method will first call theisDirty
method determines if an update is currently needed, and if it returns true, then therun
method to execute the Sub subscriber's callback function for updates. If thecomputed
、watch
、watchEffect
Waiting for the subscriber to call the run method will execute its callback function, if it is a render function this kind of subscriber calls the run method will execute the render function again.
call (programming)isDirty
method is passed in, and it's worth noting that this is a pointer to theReactiveEffect
Example. AndReactiveEffect
Also inherited fromSubscriber
subscriber, so this here is pointing to the subscriber.
As we talked about earlier, modifying responsive variablescount1
will be notified when the value ofas a subscriber(used form a nominal expression)doubleCount
Calculate the attributes. When the notificationas a subscriberInstead of going to a subscriber like watchEffect and executing its callbacks when the computed property is updated, theDependency as Depwhen subscribing to his subscribers for updates. Calculating the attributes heredoubleCount
is used in templates, so his subscriber is the render function.
So when you modify the count1 variable to execute runIfDirty, the subscriber that is triggered at this point is the render function as a Sub subscriber, which means that this is the render function at this point!
Let's see.isDirty
is how the dirty check is performed, the code is as follows:
function isDirty(sub: Subscriber): boolean {
for (let link = ; link; link = ) {
if (
!== ||
( &&
(refreshComputed() ||
!== ))
) {
return true;
}
}
return false;
}
Here's where we get into the two-way chained tables we talked about in the previous section, recalling the responsive model diagram we talked about earlier, as follows:
The sub subscriber at this point is the render function, which is the graphicalSub2
。is a pointer to a pointer to
Sub2
the head of the queue consisting of Link nodes above the subscriber's x-axis (horizontal).It is to point to the next Link node above the X-axis, and through the Link node you can access the corresponding Dep dependency.
Here the render function corresponds to the subscriberSub2
There is only one node above the x-axisLink3
。
The for loop here is to facilitate all the Link nodes of the Sub subscriber on the X-axis, and then inside the for loop to access the corresponding Dep dependency through the Link nodes to do the version counting judgment.
The if statement judgment inside the for loop here is divided into two main parts:
if (
!== ||
( &&
(refreshComputed() ||
!== ))
) {
return true;
}
As long as one of these two parts is true, then the current Sub subscriber needs to be updated, i.e., its callback is executed.
Let's look at the first judgment:
!==
Remember we talked about this earlier, the initialization will hold thecap (a poem)
The value of the variable is the same as the value of the variable. Each time the responsive variable changes it walks to the set intercept, where it will go to execute the
++
At this point, after the execution of thecap (a poem)
value is already different, where it can be known that the responsive variable has changed at this point and the Sub subscriber needs to be notified of the update to execute its callback.
The regular case where the Dep dependency is a ref variable and the Sub subscriber is wachEffect is indeed satisfied by the first judgment.
But here we are.is the computational property
doubleCount
, the computational properties are determined by theComputedRefImpl
The simplified code for the object that comes out of class NEW is as follows:
class ComputedRefImpl<T = any> implements Subscriber {
_value: any = undefined;
readonly dep: Dep = new Dep(this);
globalVersion: number = globalVersion - 1;
get value(): T {
// ...an omission
}
set value(newValue) {
// ...an omission
}
}
ComputedRefImpl
inheritedSubscriber
class, so he is said to be a subscriber. There are also get and set intercepts, and a corresponding Dep dependency that goes to new when initializing a computed property.
One other thing worth noting is that the computational property above theThe initial value of the attribute is
globalVersion - 1
The default is not equal toglobalVersion
This is so that the first time a calculated property is executed it can go and trigger the callback for executing the calculated property, which is done later in therefreshComputed
It will be talked about in the function.
We are directly modifiedcount1
variable in thecount1
variable is triggered in the set intercept of the++
but does not modify the computed attribute corresponding to the. So when calculating attributes as dependencies simply using the
!==
It would not satisfy the requirement and would need to be used to the second judgment:
( &&
(refreshComputed() ||
!== ))
In the second judgment first determine if the current current Dep dependency is a computed property, if so call therefreshComputed
function to execute the callback for the computed attribute. It then determines whether the result of calculating the attribute has changed, and if it has in therefreshComputed
function then goes and executes the++
So after the execution ofrefreshComputed
function latercap (a poem)
The value of the computed attribute is different, indicating that the value of the computed attribute has been updated, which of course requires the execution of a render function that relies on the computed attribute.
The refreshComputed function
Let's see.refreshComputed
The code for the function, simplified, is as follows:
function refreshComputed(computed: ComputedRefImpl): undefined {
if ( === globalVersion) {
return;
}
= globalVersion;
const dep = ;
try {
prepareDeps(computed);
const value = (computed._value);
if ( === 0 || hasChanged(value, computed._value)) {
computed._value = value;
++;
}
} catch (err) {
++;
throw err;
} finally {
cleanupDeps(computed);
}
}
The first thing you'll do is to judge === globalVersion
Whether they are equal or not, if they are equal it means that there was no responsive variable change at all, so of course there is no need to go and re-execute the compute attribute callback.
Remember that we talked earlier about how every time a responsive variable changes and triggers a set intercept it executes theglobalVersion++
? So here's how it can be done with === globalVersion
Determines if a responsive variable has changed, if not it means that the value of the calculated attribute must not have changed.
Then it's time to execute = globalVersion
commander-in-chief (military)The value of the synchronization is
globalVersion
, in preparation for the next determination of whether the computation property needs to be re-executed.
In a try it will go first and execute theprepareDeps
function, let's put this aside and talk about it next, let's look at the rest of the code in try first.
first callconst value = (computed._value)
Go re-execute the callback function for the calculated property to get the new return value for the calculated property.value
。
Then it's time to executeif ( === 0 || hasChanged(value, computed._value))
We talked earlier about the default value of 0 for the version above the dep, and here the === 0
The description is the first rendering of the computed attribute. The next step is to use thehasChanged(value, computed._value)
Determines whether the new value of a calculated attribute has been modified compared to the old value.
When one of these two conditions is met, the if is executed, updating the value of the new computed attribute and executing the++
. Because of what was said earlier about being out there will use !==
Determine if the version of dep is the same as the version above the link, if not, execute the render function.
Here, since the value of the compute property does change, it executes the++
The version of dep is different from the version above the link at this point, so it's marked as dirty, which executes the render function.
If there is an error executing the callback function for the computed attribute, the same is executed once++
。
Finally there is the remaining execution of the compute attribute callback function before calling theprepareDeps
and finally calls thecleanupDeps
Functions aren't spoken for.
Updating the responsive model
Review the code for the demo:
<template>
<p>{{ doubleCount }}</p>
<button @click="flag = !flag">switch modes or data streamsflag</button>
<button @click="count1++">count1++</button>
<button @click="count2++">count2++</button>
</template>
<script setup>
import { computed, ref } from "vue";
const count1 = ref(1);
const count2 = ref(10);
const flag = ref(true);
const doubleCount = computed(() => {
("computed");
if () {
return * 2;
} else {
return * 2;
}
});
</script>
(coll.) fail (a student)flag
value is true, the corresponding responsive model we've talked about earlier is shown below:
If we willflag
is set to false? At this point the calculated attributedoubleCount
would no longer rely on responsive variablescount1
, instead relying on responsive variablescount2
. Guys guess what the responsive model should look like at this point?
Now there's one more.count2
variable corresponding to theLink4
originalLink1
cap (a poem)Link2
The connection between also because the computation properties no longer depend on thecount1
After the variable, the connection between the two of them is gone in favor of theLink1
cap (a poem)Link4
A connection is established between the
I don't know what I'm talking about.prepareDeps
cap (a poem)cleanupDeps
function is to remove theLink1
cap (a poem)Link2
The connection between the
prepareDeps
The function code is as follows:
function prepareDeps(sub: Subscriber) {
// Prepare deps for tracking, starting from the head
for (let link = ; link; link = ) {
// set all previous deps' (if any) version to -1 so that we can track
// which ones are unused after the run
= -1
// store previous active sub if link was being used in another context
=
= link
}
}
Here, a for loop is used to iterate through the Link nodes above the X-axis of the computed attribute Sub1, i.e., Link1 and Link2, and theversion
attribute is set to -1.
(coll.) fail (a student)flag
value is set to false, re-execute the computation propertydoubleCount
in the callback function, a read operation is performed on all responsive variables in the callback function. From there, the get interception of the responsive variables is triggered again, and then the execution of thetrack
method for dependency collection. Notice that a new responsive variable is collected at this pointcount2
. The responsive model diagram after the collection is complete is shown below:
As you can see from the above figure although the computed property although no longer dependent on thecount1
variable, but thecount1
variable variable corresponding to theLink2
The node is still connected on the queue.
We are inprepareDeps
method sets the version attribute of all Link nodes on which the calculated attribute depends to -1 in thetrack
The method collects dependencies by executing a line of code as follows:
class Dep {
track() {
if (link === undefined || !== activeSub) {
// ...an omission
} else if ( === -1) {
= ;
// ...an omission
}
}
}
in the event that === -1
Then it's going to beThe value of the synchronization is
The value of the
Only the responsive variable on which the latest dependency of the computed property is based triggers thetrack
method for dependency collection so that the correspondingthrough (a gap)
-1
update to。
variablecount1
It's not triggered anymore.track
method now, so the variablecount1
correspondingThe value of the
-1
。
And finally, the implementationcleanupDeps
function willis still a responsive variable with a value of -1 (i.e., the no-longer-used
count1
variable) corresponding to the Link node that is given to be taken out from the bidirectional chain table. The code is as follows:
function cleanupDeps(sub: Subscriber) {
// Cleanup unsued deps
let head;
let tail = ;
let link = tail;
while (link) {
const prev = ;
if ( === -1) {
if (link === tail) tail = prev;
// unused - remove it from the dep's subscribing effect list
removeSub(link);
// also remove it from this effect's dep list
removeDep(link);
} else {
// The new head is the last node seen which wasn't removed
// from the doubly-linked list
head = link;
}
// restore previous active link if any
= ;
= undefined;
link = prev;
}
// set the new head & tail
= head;
= tail;
}
Iterate over the Link nodes above the horizontal queue (X-axis) of Sub1 computed attributes when the === -1
When this is the case, it means that the Dep dependency corresponding to this Link node is no longer dependent on the computed attribute, so the execution of theremoveSub
cap (a poem)removeDep
Remove it from the two-way chain table.
executedcleanupDeps
The responsive model at this point after the function is what we mentioned earlier, as shown below:
summarize
There are four main versions of version counting: global variablesglobalVersion
、、
cap (a poem)
。
cap (a poem)
If they are not equal it means that the value of the current responsive variable has changed and you need to get the Sub subscriber to update it.
If it's a computed property as Dep dependency it can't be passed through thecap (a poem)
Goes to judgment, but instead executes
refreshComputed
function to make a judgment. In therefreshComputed
function will first determine theglobalVersion
cap (a poem)If they are equal, then there is no responsive variable update. If not, then the callback function for the computed property is executed, which gets the latest value and compares it to the computed property to see if the value has changed. It also executes the
prepareDeps
cap (a poem)cleanupDeps
function removes the Link nodes corresponding to responsive variables on which the computed attributes no longer depend from the bidirectional chain table.
As a final note, the biggest winner of version counting is the computed calculation property, although the code is much harder to understand with the introduction of version counting. But the overall process is more elegant, as well as the fact that now you only need to determine if a few versions are equal to know if the subscriber needs to update, and of course the performance is better.
Follow the public number: [Front-end Ouyang], give yourself a chance to advance vue
Also Ouyang wrote an open source ebookvue3 Compilation Principles Revealed, reading this book can give you a qualitative improvement in your knowledge of vue compilation. This book is accessible to beginner and intermediate front-ends and is completely free, just asking for a STAR.