preface

An important function of the vue data response type, namely when the data update, to update the template of the vue automatically all the all the data associated with the modified data, there is a key point, templates, how to know what data to use, data, how do you know where the template used to myself, this is two very important things in vue, Collect dependencies and distribute updates.

Responsive optimization of vue3.2

In vue3.2, there is a very significant update, vue. Js3.2, which introduces reactive performance optimization,

There is an introduction in the text, baidu translation is direct

  • More efficient REF implementation (read speed up about 260%, write speed up about 50%)
  • Dependency tracking speed increased by about 40%
  • Memory usage is reduced by about 17%
  • Some improvements have also been made to the template compiler:
  • Creating normal element VNodes is about 200% faster
  • Be more active in continuous improvement

This awesome performance boost was proposed by @Basvanmeurs, a GitHub biggie, to briefly explain how it works:

A few things to start with:

  1. effectTrackDepthLog and control the number of dependency nesting layers
  2. maxMarkerBitsDependencies may be nested within dependencies. This is used to limit dependency nesting to up to 30 levels, but this is generally not achieved
  3. trackOpBitAttribute of a dependency, dependenteffectTrackDepthGenerate, how to verify that the current dependency is a collected dependency, a new collection of dependencies, all based on this flag bit operation,

A wasTracked function that validates a collected dependency, or a newTracked function that validates a new collected dependency. TrackOpBit relies on effectTrackDepth. TrackOpBit = 1 << ++effectTrackDepth = 1 << ++effectTrackDepth = 1 << ++effectTrackDepth = 1 << ++effectTrackDepth = 1 << ++effectTrackDepth = 1 << ++effectTrackDepth = 1 << ++ Dep. N & trackOpBit = 2, dep. N & trackOpBit = 2, dep. N & trackOpBit = 2, dep. N & trackOpBit = 2, deP. N & trackOpBit = 2, deP. You don’t need to add or delete anything else, you just need to find what you need among all the dependencies.

How are dependencies nested and how are relationships generated

Take a look at the following example

const count = ref(0);
const double = computed(() = > {
  return 8 * computed(() = > {
    return count * 2;
  });
});
Copy the code

Computed data generates dependencies, that is, when computed data in the outer layer is executed, the data used depends on computed results in the inner layer, which results in dependency nesting. The structure of dependency nesting is similar to that of a tree, as shown in the figure belowThe first layer oftrackOpBitPhi is 2 if phi is usedwasTrackedDelta function or delta functionnewTrackedIf the value of w and n in the function is 4, it means that the dependency is nested. This is a good distinction between dependencies and dependencies. Other optimizations will wait until the later collection of dependencies and distribution of updates, and then continue to say

Other optimization related

Several tool methods
// Determine whether tracing should be done
export function isTracking() {
  returnshouldTrack && activeEffect ! = =undefined
}

// Stop tracking globally
export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

// Global tracing is possible
export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

// Revert to before enableTracking() or pauseTracking()
export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

// x & y > 0 x = y 2 & 4 = 0 2 & 2 = 2
// Verify that it is a collected dependency
export const wasTracked = (dep: Dep): boolean= > (dep.w & trackOpBit) > 0

// Verify that it is a newly collected dependency
export const newTracked = (dep: Dep): boolean= > (dep.n & trackOpBit) > 0

// Initialize deP flags as collected dependencies
export const initDepMarkers = ({ deps }: ReactiveEffect) = > {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // Set was tracked // Flag dependencies are collected}}}// Remove effect from all dependent dePs
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0}}Copy the code
Several global variables
// Record the current number of layers
let effectTrackDepth = 0

// effect attribute flag
export let trackOpBit = 1

// effect Maximum number of dependency nesting layers (nesting is like using computed in a dependency with other dependencies, for example, computed)
// Up to 30 layers are supported
const maxMarkerBits = 30

// Global effectStack
const effectStack: ReactiveEffect[] = []

// Effect currently active
let activeEffect: ReactiveEffect | undefined
Copy the code

Collect rely on

Let’s take a look at vue’s collection dependencies first. Some of you may be wondering what a dependency is. First of all, a vue template at the time of rendering into a page (all for convenience, the environment is chrome) must have the data, or the page is a blank page, which means the page display depends on the data, it is called, and collect is dependent on the page render time, using the data collected.

Collected but not to collect the data itself, but rather to collect data to render himself to the page or other use of the function to the data of the function, these functions have a unified name: side effect function, side effects is not here, is simply a function is side effect function to function outside of any use. This is where the rendering function refers to the external data

<div id="app">
  <p @click="addCount">{{ count }}</p>
</div>
<script src="https://unpkg.com/vue/vue@next"><script>
<script> const {createApp, ref} = Vue const App = { setup() { const count = ref(0) function addCount() { count.value++ } } } const app = createApp(App) app.mount('#app') </script>
Copy the code

This is a simple VueApp, using this example to talk about collecting dependencies, first converting the template to render function

const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue

const _hoisted_1 = { id: "app" }
const _hoisted_2 = ["onClick"]

return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("div", _hoisted_1, [
      _createElementVNode("p", { onClick: addCount }, _toDisplayString(count), 9 /* TEXT, PROPS */, _hoisted_2)
    ]))
  }
}

// Check the console for the AST
Copy the code

_toDisplayString(count) triggers three get interceptor methods: get for getting a data object (the object returned by setup will be propped), get for getting count from the data object, Count itself get,

In each time you take out the data from the data object setupSatate, will first through RuntimeCompiledPublicInstanceProxyHandlers, at this moment the vue rendering function is the only interface to get the data from the outside, This interface will be called PublicInstanceProxyHandlers to obtain data, in PublicInstanceProxyHandlers for users trying to access data in a series of processing, judge whether the data can be modified or get, if the data is an array, Each value in the array is processed, perhaps so that it can later confirm what changes have taken place in the array before being returned to the render function. In this process, the get interceptor method of the data itself is triggered. In this example, because count is ref type, the dependency collection process is followed

First take a look atrefIs how to intercept value fetching and modifying,refA container holds only one value. Interceptors are access descriptors, i.egetterandsetter. In the render function execution, the data is executedgetter, go back to calltrackRefValue.

TrackRefValue acts as an entry point for ref collection dependencies, with some pre-processing: IsTracking () only collects dependencies that are globally allowed to trace. The purpose of this is to prevent dependencies from being collected in vue3. Dependencies need to be cleared before each dependency is collected, improving performance, and some basic processing: Take the original ref and initialize the DEP on the ref. There are two markers on the DEP, w: indicates that the dependency has been collected and N: indicates that the dependency is newly collected. These two markers are used as an important basis for whether the dependency should be executed when the update is sent out later

Finally, the core logic trackEffect is executed

trackEffect

Before the formal collection rely on, will continue to do some validation, such as nested have beyond the limits, beyond the limit is the clear pattern, is a new collection of dependence, not the tag is the new collection of dependence (because if here is collecting the dependence), to verify whether dependence on collection, not only should to collect dependence, in the end if it is to rely on collection, The process of collecting dependencies is complete,

Distributed update

A page always needs to interact with the user, and most of the interaction is data changes. In this case, we need to use previously collected dependencies. By modifying the data, these dependencies are triggered to run

<div id="app">
  <p @click="addCount">{{ count }}</p>
</div>
<script src="https://unpkg.com/vue/vue@next"><script>
<script> const {createApp, ref} = Vue const App = { setup() { const count = ref(0) function addCount() { count.value++ } } } const app = createApp(App) app.mount('#app') </script>
Copy the code

As soon as I trigger the click event that causes the data to change, the intercepting method starts acting, and the setter intercepts the changes to the data to trigger the entry function triggerRefValue that sends out updates

triggerRefValueThe function doesn’t do a lot of processing heretriggerEffectFunction, depending on whether the dependency is queued or executed directly, the process of issuing updates completes,

At this time, we will find that the execution is actually run function or scheduler function in Effect. Therefore, the real core is how effect is generated and what optimization process is done inside, which is the important point of our study.

ReactiveEffect

In vue3, there was a function called createReactiveEffect, but at that time only one function was defined. After vue3.2, it was created by instantiation. Vue3.2 internally implements a ReactiveEffect class, which is parsed in parts below

The constructor

Fn is a side effect function that needs to be executed for data changes, but sometimes FN may already be effect, so it is necessary to find the original function of FN.schedulerIs the function that the side effect function executes on the queue,recordEffectScopeIt is to belong toEffectScopeThe processing, here is not repeated, interested can go to seeeffectScope.tsfile

runfunction

runA function is the beginning of a dependency execution,schedulerThe inside is actually calledrunFunctions, whose dependent execution mimics the way functions are pushed and unloaded, have a global oneeffectStackAs an “execution stack” for Effect, whenever there is oneeffectWhen you start executing, you entereffectStackThat’s the push, all the way to the wholeeffectAfter the execution,effectIt’s going to be removedeffectStackThis is the unstack.

This also prevents a dependency from executing more than once and verifies that it is already executing before effect is executed. If not, go to normal process, first enter the stack, open tracking, create a new trackOpBit, determine whether more than nested limits, more than the clear pattern will go, but is generally not more than, normal is marked as dependence on collection, began to perform side effect function, this dependence may trigger other dependence, if they are nested, Before the nested dependency executes, it adds one, and when the dependency executes, it goes back, and trackOpBit goes back. This allows you to distinguish dependencies. InitDepMarkers the dependency to be executed as collected,

Some handling after the side effect function completes: As mentioned above, data will be used in the process of page rendering, but some data will not affect the page, but they are still collected in the initial rendering process. In VUe3, all dependencies are deleted and collected again, which is a waste of performance. After optimization, More use of tags on dependencies to determine, less on delete and add operations, improving performance

The last effect is removed from the stack and returned to enableTracking() or pauseTracking(). The last effect on the stack is removed and the run function is completed

The stop function

This function is simple, just to stop the dependency, and clear all information about effect itself, if there is an onStop function after the execution

Other properties of the class

Deps: is the current side effect used by the DEP in its own, easy to read directly

Computed: Computed generates different dependencies than watch, etc., and needs to do some special processing (in computed. Ts file)

AllowRecurse: Allows nesting

andReactiveEffectThis API

Sometimes, users want to customize a function that executes as the data changes, but instantiating a ReactiveEffect is a bit of a chore and provides several apis

Effect the function

The effect function can easily generate an effect, and will return an Effect runner, which can be used to stop the side effect function from executing as the data changes.

It can also be run manually at some point after having the custom side effect function depend on the data, with one parameteroptionstoeffectPass configuration, such as lazy loading, the function is run immediately if there is no lazy loading or no pass configuration. The implementation core depends primarily onReactiveEffectClass.

The stop function

effectAt the end of the function execution, returnseffect runnerandeffectThere will be cross-references itself, which can be very convenient to stop the execution of the side effect function, which can be directlyrunner.effect.stop(), can also be calledstop()function

Other responsive apis collect dependencies and distribute updates

The track and trigger functions, like trackRefValue and triggerRefValue, are handled differently from the ref’s separate dependency on collecting values and distributing updates. They all end up calling trackEffect and triggerEffect, but with different processing done before. Before this, we will introduce a targetMap object that stores data in the effect dependency relationship. This is a WeakMap object. The reason why WeakMap is used is that the browser will automatically garbage collect the data that WeakMap may not be used inside. Structure is as follows

{
  target1: {
    key1: {
      effect1,
      effect2
      ...
    },
    key2: {

    }
    ...
  },
  target2: {
    
  }
  ...
}
Copy the code

Track function

In cases where collecting dependencies is globally permitted, it goes firsttargetMapTry to find if the current dependency exists. If it does not exist, create one by one and pass it totrackEffectWhere deP is created throughcreateDep.And mark these dependencies as newly collected dependencies

This is for packaging dependencies, easy coordination optimization, and DEP is aSetObject so that side effects are not added.

The trigger function

The implementation process of trigger is quite complicated. We analyze it step by step.

First of all, make sure that the dependent data exists, and the dependent data does not exist.

Update can also be divided into different cases, the first case is the emptying operation, such as group emptying, Map emptying and Set emptying, which is to execute all dependencies directly, the second case is the data array and new operation, will find all new dependencies in the array.

ITERATE_KEY = MAP_KEY_ITERATE_KEY; ITERATE_KEY = MAP_KEY_ITERATE_KEY; ITERATE_KEY = MAP_KEY_ITERATE_KEY; If it is an array, it will be collected with the length dependency (array length is also a dependency, delete and ADD items will affect the length). Because arrays have no DELETE and set methods, the other two cases call their own delete and set methods

The final call to triggerEffect can be either two nested dependencies generated by data changes or multiple dependencies generated by pure data changes, each of which also needs to be wrapped using creatDep.

conclusion

Responsive optimization can greatly improve the performance of VUE, mainly in the following aspects:

  1. Ref does not use track to collect dependencies, but rewrites a method, trackRefValue, and puts all collected dependencies in its own DEPS. When it needs to distribute updates, it only needs to traverse the DEPS. Performance is improved by eliminating the need to perform track’s complex lookups, while track still puts all its dependencies in the global variable target

  2. To rely on packaging, w and n two tags bit operations, can better management rely on, don’t need to empty all depend on every time, and then to collect depend on, can be achieved by w and n depend on whether it is need two tags judgment, to reduce the use of memory, (no need to Set several operations, the Map)

  3. Implementation of ReactiveEffect, so that effect production and management can be better.

This optimization is not just for vUE, but for all VueApp.

The above is my understanding of collecting dependencies and distributing updates. If there is something wrong or missing, please point it out. If you have a better understanding, I hope you can explain it in the comments section.