Location>code7788 >text

Vue 3.5 Responsive Refactoring "Version Counting" for 56% Performance Improvement

Popularity:191 ℃/2024-11-06 09:56:31

preamble

Vue 3.5 responsive refactoring is divided into two main parts:bidirectional linked listcap (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 subscribersDep dependencyLink 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 subscriberscap (a poem)Dep dependencyThe Bridge Between.Sub subscriberswould like to visitDep dependencypassLink nodeas wellDep dependencywould like to visitSub subscribersIt can only be done byLink node

Careful peeps may have noticed that the computed computation property is not only about theSub subscribersneverthelessDep dependency
The reason for this is that computed can be used likewatchEffectThat 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 variableglobalVersioncap (a poem)

  • globalVersionis a global variable with an initial value of0, triggered only when a responsive variable is changedglobalVersion++

  • Yes, it is.depDepends 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 thehas the same value. Before responsive updating is when you pass thecap (a poem)is the same to determine if an update is needed.

  • : Calculate the version above the property if === globalVersionIt 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 * 2nevertheless * 2

The question then arises whenflagThe value of thetrueWhen clicking on thecount2++button.("computed")Will it perform a print? That is.doubleCountvalue will be recalculated?

The answer is:will not (act, happen etc). Althoughcount2is 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 countup, and we'll talk about it in detail next.

dependent trigger

It's the same demo as before, initializing theflagvalue is true, so in computed there will be a change to thecount1variable performs a read operation and then triggers a get intercept.count1This ref responsive variable is the one used by theRefImplClass 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()whichdepattributableDepThe 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++;
    ();
  }
}

existtrackmethod using theLinkclass new out of a link object.LinkThe 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 theversionattribute, the other attributes were covered in the previous article on bidirectional linked lists.

existconstructorhit the nail on the headdo sth (for sb)Assigning a value ensures that thecap (a poem)values are equal, that is, they are equal to 0. Because thewhich has an initial value of 0, will be covered next.

When we click on thecount1++button will make the responsive variablecount1values are self-increasing. Since thecount1is 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()triggerThe 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.globalVersionis a global variable with an initial value of 0.

Dep aboveversionThe initial value of the attribute is also 0.

existtriggerwere executed separately in the++cap (a poem)globalVersion++This is the dep to which this is pointing after the execution ofcap (a poem)globalVersionvalue is now 1. And at this pointis still 0, this timecap (a poem)values would already be unequal.

Then it's time to executenotifymethod notifies subscribers of updates according to the new responsive model, which at this point in our example is shown below:
reactive

If a modified responsive variable triggers multiple subscribers, such as thecount1variable is used by more than onewatchEffectUse, modifycount1The value of the variable would then need to trigger updates from multiple subscribers.notifymethod 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 thenotifymethod, you only need to know the contents of the method that executes thenotifymethod then triggers an update from the subscriber.

(These two paragraphs arenotify(the logic within the method) follows the normal logic if thecount1A change in the value of a variable can be made with theLink2Node FindingSub1subscriber, and then executes the subscriber'snotifymethod and thus update it.

If ourSub1The subscriber is the render function, is this normal logic. But at this point ourSub1Subscribers are computing propertiesdoubleCountInstead 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 attributedoubleCountis used in the template, so the computed attributedoubleCountof the subscriber is the render function.

So here is the call to()Calculated properties will not be triggereddoubleCountInstead of re-executing the callback function, it goes and triggers the computation of the propertydoubleCountof 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 subscribersThe internals are all based onReactiveEffectclass to implement it, calling the subscriber'snotifymethod to notify the update is actually underlying the call to theReactiveEffectclassrunIfDirtymethod. The code is as follows:

class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
  /**
   * @internal
   */
  runIfDirty(): void {
    if (isDirty(this)) {
      ();
    }
  }
}

existrunIfDirtymethod will first call theisDirtymethod determines if an update is currently needed, and if it returns true, then therunmethod to execute the Sub subscriber's callback function for updates. If thecomputedwatchwatchEffectWaiting 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)isDirtymethod is passed in, and it's worth noting that this is a pointer to theReactiveEffectExample. AndReactiveEffectAlso inherited fromSubscribersubscriber, so this here is pointing to the subscriber.

As we talked about earlier, modifying responsive variablescount1will be notified when the value ofas a subscriber(used form a nominal expression)doubleCountCalculate 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 heredoubleCountis 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.isDirtyis 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:
reactive
The sub subscriber at this point is the render function, which is the graphicalSub2is a pointer to a pointer toSub2the 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 subscriberSub2There 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 propertydoubleCount, the computational properties are determined by theComputedRefImplThe 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
  }
}

ComputedRefImplinheritedSubscriberclass, 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 isglobalVersion - 1The default is not equal toglobalVersionThis 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 therefreshComputedIt will be talked about in the function.

We are directly modifiedcount1variable in thecount1variable 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 therefreshComputedfunction 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 therefreshComputedfunction then goes and executes the++So after the execution ofrefreshComputedfunction 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.refreshComputedThe 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 === globalVersionWhether 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 === globalVersionDetermines 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 = globalVersioncommander-in-chief (military)The value of the synchronization isglobalVersion, 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 theprepareDepsfunction, 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 === 0The 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 theprepareDepsand finally calls thecleanupDepsFunctions 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)flagvalue is true, the corresponding responsive model we've talked about earlier is shown below:
reactive

If we willflagis set to false? At this point the calculated attributedoubleCountwould no longer rely on responsive variablescount1, instead relying on responsive variablescount2. Guys guess what the responsive model should look like at this point?
reactive2

Now there's one more.count2variable corresponding to theLink4originalLink1cap (a poem)Link2The connection between also because the computation properties no longer depend on thecount1After the variable, the connection between the two of them is gone in favor of theLink1cap (a poem)Link4A connection is established between the

I don't know what I'm talking about.prepareDepscap (a poem)cleanupDepsfunction is to remove theLink1cap (a poem)Link2The connection between the

prepareDepsThe 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 theversionattribute is set to -1.

(coll.) fail (a student)flagvalue is set to false, re-execute the computation propertydoubleCountin 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 thetrackmethod 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:
reactive3

As you can see from the above figure although the computed property although no longer dependent on thecount1variable, but thecount1variable variable corresponding to theLink2The node is still connected on the queue.

We are inprepareDepsmethod sets the version attribute of all Link nodes on which the calculated attribute depends to -1 in thetrackThe 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 === -1Then it's going to beThe value of the synchronization isThe value of the

Only the responsive variable on which the latest dependency of the computed property is based triggers thetrackmethod for dependency collection so that the correspondingthrough (a gap)-1update to

variablecount1It's not triggered anymore.trackmethod now, so the variablecount1correspondingThe value of the-1

And finally, the implementationcleanupDepsfunction willis still a responsive variable with a value of -1 (i.e., the no-longer-usedcount1variable) 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 === -1When 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 theremoveSubcap (a poem)removeDepRemove it from the two-way chain table.

executedcleanupDepsThe responsive model at this point after the function is what we mentioned earlier, as shown below:
reactive2

summarize

There are four main versions of version counting: global variablesglobalVersioncap (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 executesrefreshComputedfunction to make a judgment. In therefreshComputedfunction will first determine theglobalVersioncap (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 theprepareDepscap (a poem)cleanupDepsfunction 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.