Vue.js on the optimization of the responsive part

Posted by xwhitchx on Sat, 25 Dec 2021 19:05:26 +0100

It has been almost a year since Vue 3 was officially released. I believe many small partners have used Vue 3 in the production environment. Today, Vue JS 3.2 has been officially released, and this minor version upgrade is mainly reflected in the optimization of the source code level. In fact, it has not changed much for the user's use level. One of the things that attracted me was the improvement of responsive performance:

More efficient ref implementation (~260% faster read / ~50% faster write)
~40% faster dependency tracking
~17% less memory usage

The read efficiency of ref API is improved by about 260%, the write efficiency is improved by about 50%, the efficiency of dependent collection is improved by about 40%, and the memory usage is reduced by about 17%.

This is an optimization, because you should know that the responsive system is Vue One of the core implementations of. JS. Optimizing it means using Vue.js for all JS development of App performance optimization.

The responsive performance optimization proposed by basvan meurs really makes you very happy. It not only greatly improves the runtime performance of Vue 3, but also because such core code can be contributed by the community, which means that Vue 3 has attracted more and more attention; Some competent developers participate in the contribution of core code, which can make Vue 3 go further and better.

As we know, compared with Vue 2, Vue 3 has been optimized in many aspects, one of which is the implementation of data response, which is implemented by object The defineproperty API has been changed to the Proxy API.

Since Proxy is slow, why did Vue 3 choose it to implement data response? Because Proxy essentially hijacks an object, it can not only monitor the change of an object's attribute value, but also monitor the addition and deletion of object attributes; And object Defineproperty is to add a corresponding getter and setter to an existing property of an object, so it can only monitor the change of the property value, not the addition and deletion of object properties.

The performance optimization of responsiveness is actually reflected in the scene of changing deeply nested objects into responsiveness. In the implementation of Vue 2, when the data is changed into a response in the component initialization phase, if the sub attribute is still an object, the object will be executed recursively Defineproperty defines the response expression of the sub object; In the implementation of Vue 3, only when the object attribute is accessed will the type of sub attribute be judged to decide whether to execute reactive recursively. In fact, this is a response implementation of delaying the definition of sub object, which will improve the performance to a certain extent.

Therefore, compared with Vue 2, Vue 3 has indeed made some optimization in the responsive implementation part, but the actual effect is limited. And Vue JS 3.2 this time, the optimization of responsive performance has really made a qualitative leap. Next, let's take some hard dishes to analyze the specific optimizations made from the source code level and the technical thinking behind these optimizations.

Principle of responsive implementation
The so-called responsive means that we can do something automatically after modifying the data; The rendering corresponding to the component is to automatically trigger the re rendering of the component after modifying the data.

Vue 3 implements the response type. In essence, it hijacks the reading and writing of data objects through the Proxy API. When we access data, it will trigger getter s to perform dependency collection; When modifying data, setter dispatch notification will be triggered.

Next, let's briefly analyze the implementation of dependency collection and dispatch notifications (vue.js before 3.2).

Dependency collection
First, let's look at the dependency collection process. The core is to trigger the getter function when accessing responsive data, and then execute the track function to collect dependencies:

let shouldTrack = true
// Currently active effect
let activeEffect
// Raw data object map
const targetMap = new WeakMap()
function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // Each target corresponds to a depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // Each key corresponds to a dep set
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // Collect the currently active effect as a dependency
    dep.add(activeEffect)
   // The currently active effect collects dep sets as dependencies
    activeEffect.deps.push(dep)
  }
}

Before analyzing the implementation of this function, we first think about the dependencies to be collected. Our purpose is to realize the response, that is, we can automatically do some things when the data changes, such as executing some functions, so the dependencies we collect are the side-effect functions executed after the data changes.

The track function has three parameters, where target represents the original data; Type indicates the type of dependency collection this time; key indicates the accessed attribute.

A global targetMap is created outside the track function as the Map of the original data object. Its key is target and its value is depsMap as the dependent Map; The key of this depsMap is the key of the target, and the value is the dep set. The dep set stores the dependent side effect functions. For ease of understanding, the relationship between them can be represented by the following figure:

Therefore, each time the track function is executed, the currently activated side effect function activeEffect is taken as the dependency, and then collected into the dependency set dep under the corresponding key of the depsMap related to the target.

Distribution notice
The dispatch notification occurs in the data update stage. The core is to trigger the setter function when modifying the responsive data, and then execute the trigger function to dispatch the notification:

const targetMap = new WeakMap()
function trigger(target, type, key) {
  // Get the dependency set corresponding to the target through targetMap
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // No dependency, return directly
    return
  }
  // Create a collection of running effects
  const effects = new Set()
  // Function to add effects
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }
  // One of the SET | ADD | DELETE operations, add the corresponding effects
  if (key !== void 0) {
    add(depsMap.get(key))
  }
  const run = (effect) => {
    // Scheduling execution
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    }
    else {
      // Direct operation
      effect()
    }
  }
  // Traversal execution effects
  effects.forEach(run)
}

The trigger function has three parameters, where target represents the target original object; Type indicates the type of update; key indicates the attribute to be modified.

The trigger function mainly does four things:

Get the dependency set depsMap corresponding to the target from the targetMap;

Create a collection of running effects;

Find the corresponding effect from depsMap according to the key and add it to the effects set;

Traverse effects to execute the related side effect functions.

Therefore, each time the trigger function is executed, all the related side-effect functions are found from the targetMap according to the target and key, and are traversed and executed.

In the process of describing dependency collection and distribution notification, we all mentioned a word: side effect function. In the process of dependency collection, we take activeEffect (currently activated side effect function) as dependency collection. What is it? Next, let's take a look at the true face of side effect function.

Side effect function
So, what is a side effect function? Before introducing it, let's review the original requirements of the response, that is, we can automatically do something after modifying the data, for example:

import { reactive } from 'vue'
const counter = reactive({
  num: 0
})
function logCount() {
  console.log(counter.num)
}
function count() {
  counter.num++
}
logCount()
count()

We defined the responsive object counter, and then accessed the counter in logCount Num, we want to modify the counter after executing the count function When the value of num is, the logCount function can be automatically executed.

According to our previous analysis of the dependency collection process, if logCount is activeEffect, the requirements can be implemented, but it is obviously impossible because the code is executing to the console Log (counter. Num), it knows nothing about its operation in the logCount function.

So what should I do? In fact, as long as we assign logCount to activeEffect before running logCount function:

`activeEffect = logCount
logCount()`

Following this idea, we can use the idea of higher-order function to encapsulate logCount:

function wrapper(fn) {
  const wrapped = function(...args) {
    activeEffect = fn
    fn(...args)
  }
  return wrapped
}
const wrappedLog = wrapper(logCount)
wrappedLog()

wrapper itself is also a function. It takes fn as a parameter, returns a new function wrapped, and then maintains a global variable activeEffect. When wrapped is executed, set activeEffect to fn, and then execute fn.

In this way, after we execute wrappedLog, we can modify counter Num, the logCount function will be executed automatically.

In fact, Vue 3 adopts a similar approach. There is an effect side effect function inside it. Let's take a look at its implementation:

// Global effect stack
const effectStack = []
// Currently active effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    // If fn is already an effect function, it points to the original function
    fn = fn.raw
  }
  // Create a wrapper, which is a reactive side effect function
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // Lazy configuration is used to calculate attributes, and non lazy is directly executed once
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if (!effect.active) {
      // In the inactive state, it is judged that if it is not scheduled for execution, the original function will be executed directly.
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      // Clear dependency of effect reference
      cleanup(effect)
      try {
        // Enable global shouldTrack to allow dependency collection
        enableTracking()
        // Stack pressing
        effectStack.push(effect)
        activeEffect = effect
        // Execute original function
        return fn()
      }
      finally {
        // Out of stack
        effectStack.pop()
        // Restore the state before shouldTrack is turned on
        resetTracking()
        // Point to the last effect on the stack
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++
  // Identity is an effect function
  effect._isEffect = true
  // The state of the effect itself
  effect.active = true
  // Wrapped primitive function
  effect.raw = fn
  // The dependency corresponding to an effect is a bidirectional pointer. The dependency contains a reference to the effect, and the effect also contains a reference to the dependency
  effect.deps = []
  // Related configuration of effect
  effect.options = options
  return effect
}

In combination with the above code, effect creates a new effect function by executing the createReactiveEffect function, Front end training In order to distinguish it from the external effect function, we call it reactiveEffect function, and add some additional attributes (I indicated in the comments). In addition, the effect function also supports passing in a configuration parameter to support more feature s, which will not be expanded here.

The reactiveEffect function is a reactive side effect function. When the trigger process sends a notification, the effect executed is it.

According to our previous analysis, the reactiveEffect function only needs to do two things: let the global activeEffect point to it, and then execute the wrapped original function fn.

But in fact, its implementation is more complex. First, it will judge whether the state of the effect is active. This is actually a control means. It is allowed to directly execute the original function fn and return in the inactive state and non scheduled execution.

Then judge whether the effect stack contains the effect. If not, press the effect into the stack. As mentioned earlier, as long as activeEffect = effect is set, why design a stack structure here?

In fact, the following nested effect s are considered:

import { reactive} from 'vue' 
import { effect } from '@vue/reactivity' 
const counter = reactive({ 
  num: 0, 
  num2: 0 
}) 
function logCount() { 
  effect(logCount2) 
  console.log('num:', counter.num) 
} 
function count() { 
  counter.num++ 
} 
function logCount2() { 
  console.log('num2:', counter.num2) 
} 
effect(logCount) 
count()

Each time we execute the effect function, if we only assign the reactiveEffect function to the activeEffect, then for this nested scenario, after executing the effect(logCount2), the activeEffect is still the reactiveEffect function returned by the effect(logCount2), so we can access the counter In num, it is wrong to rely on collecting the corresponding activeEffect. At this time, we execute the count function externally to modify the counter After num, the logCount function is not executed, but the logCount2 function. The final output is as follows:

num2: 0
num: 0
num2: 0

The expected results should be as follows:

num2: 0
num: 0
num2: 0
num: 1

Therefore, for the scenario of nested effects, we cannot simply assign activeEffect. We should consider that the execution of the function itself is a stack in and stack out operation. Therefore, we can also design an effectStack, so that each time we enter the reactiveEffect function, we first put it on the stack, and then the activeEffect points to the reactiveEffect function, Then, after fn is executed, exit the stack, and then point the activeEffect to the last element of the effectStack, that is, the reactiveEffect corresponding to the outer effect function.

Here, we also notice a detail. Before entering the stack, the cleanup function will be executed to clear the dependencies corresponding to the reactiveEffect function. When executing the track function, in addition to collecting the currently active effect as a dependency, it also uses activeEffect deps. Push (DEP) takes dep as the dependency of active effect, so that we can find the dep corresponding to the effect during cleanup, and then delete the effect from these DEPs. The code of the cleanup function is as follows:

function cleanup(effect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

Why do I need cleanup? If you encounter this scenario:

<template>
  <div v-if="state.showMsg">
    {{ state.msg }}
  </div>
  <div v-else>
    {{ Math.random()}}
  </div>
  <button @click="toggle">Toggle Msg</button>
  <button @click="switchView">Switch View</button>
</template>
<script>
  import { reactive } from 'vue'

  export default {
    setup() {
      const state = reactive({
        msg: 'Hello World',
        showMsg: true
      })

      function toggle() {
        state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World'
      }

      function switchView() {
        state.showMsg = !state.showMsg
      }

      return {
        toggle,
        switchView,
        state
      }
    }
  }
</script>

According to the code, the view of this component will display msg or a random number according to the control of the showMsg variable. When we click the Switch View button, the variable value will be modified.

Assuming there is no cleanup, when rendering the template for the first time, activeEffect is the side effect rendering function of the component, because the state is accessed when rendering the template MSG, so it will execute dependency collection and take the side effect rendering function as state MSG dependency, we call it render effect. Then we click the Switch View button, and the view will be switched to display random numbers. At this time, we click the Toggle Msg button again, because the state is modified MSG will send a notification, find the render effect and execute it, which will trigger the re rendering of the component.

However, this behavior does not meet the expectation, because when we click the Switch View button and the view is switched to display random numbers, it will also trigger the re rendering of components, but the view does not render state at this time MSG, so changes to it should not affect the re rendering of components.

Therefore, before the render effect of the component is executed, if the dependency is cleaned up, we can delete the previous state The render effect dependency collected by MSG. So when we modify state MSG, because there is no dependency, it will not trigger the re rendering of components, which is in line with expectations.

Optimization of responsive implementation
The principle of responsive implementation has been analyzed. It seems that everything is OK. What else can be optimized?

Optimization of dependent collection
At present, every time the side-effect function is executed, it is necessary to clean up the dependencies, and then re collect the dependencies during the execution of the side-effect function. This process involves a large number of addition and deletion operations on the Set set. In many scenarios, dependencies are rarely changed, so there is a certain optimization space.

In order to reduce the addition and deletion of collections, we need to identify the status of each dependent collection, such as whether it is newly collected or has been collected.

Therefore, two attributes need to be added to the set dep:

export const createDep = (effects) => {
  const dep = new Set(effects)
  dep.w = 0
  dep.n = 0
  return dep
}

Where w indicates whether it has been collected and n indicates whether it is newly collected.

Then design several global variables, effectTrackDepth, trackOpBit and maxMarkerBits.

Where effectTrackDepth represents the depth of recursive nested execution of the effect function; trackOpBit is used to identify the status of dependency collection; maxMarkerBits indicates the number of bits of the maximum marker.

Next, let's look at their applications:

function effect(fn, options) {
  if (fn.effect) {
    fn = fn.effect.fn
  }
  // Create_ effect instance 
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // Copy attributes in options to_ In effect
    extend(_effect, options)
    if (options.scope)
      // effectScope related processing logic
      recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    // Execute now
    _effect.run()
  }
  // Bind the run function as the effect runner
  const runner = _effect.run.bind(_effect)
  // Keep pairs in runner_ Reference to effect
  runner.effect = _effect
  return runner
}

class ReactiveEffect {
  constructor(fn, scheduler = null, scope) {
    this.fn = fn
    this.scheduler = scheduler
    this.active = true
    // effect storage related deps dependencies
    this.deps = []
    // effectScope related processing logic
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        // Stack pressing
        effectStack.push((activeEffect = this))
        enableTracking()
        // Record the number of bits according to the recursive depth
        trackOpBit = 1 << ++effectTrackDepth
        // If it exceeds maxMarkerBits, the calculation of trackOpBit will exceed the maximum integer digits and be degraded to cleanupEffect
        if (effectTrackDepth <= maxMarkerBits) {
          // Mark dependencies
          initDepMarkers(this)
        }
        else {
          cleanupEffect(this)
        }
        return this.fn()
      }
      finally {
        if (effectTrackDepth <= maxMarkerBits) {
          // Complete dependency tag
          finalizeDepMarkers(this)
        }
        // Restore to previous level
        trackOpBit = 1 << --effectTrackDepth
        resetTracking()
        // Out of stack
        effectStack.pop()
        const n = effectStack.length
        // Point to the last effect on the stack
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

It can be seen that the implementation of the effect function has been modified and adjusted to some extent, and a ReactiveEffect class has been created internally_ Effect instance, and the runner returned by the function points to the run method of the ReactiveEffect class.

That is, when the side effect function effect function is executed, the run function is actually executed.

When the run function is executed, we notice that the cleanup function is no longer executed by default. Before the encapsulated function fn is executed, first execute trackOpBit = 1 < < + + effecttrackdepth, record trackOpBit, and then compare whether the recursion depth exceeds maxMarkerBits, If it exceeds (usually not), the old cleanup logic will still be executed. If it does not exceed, initDepMarkers will be executed to mark the dependency to see its implementation:

const initDepMarkers = ({ deps }) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // Tag dependencies have been collected
    }
  }
}

The initDepMarkers function is simple to implement and can be traversed_ The deps attribute in the effect instance marks the w attribute of each dep as the value of trackOpBit.

Next, the fn function will be executed, which is the function encapsulated by the side effect function. For example, for component rendering, fn is the component rendering function.

When fn function is executed, responsive data will be accessed, their getter s will be triggered, and then track function will be executed to collect dependencies. Accordingly, some adjustments have been made to the process of dependent collection:

function track(target, type, key) {
  if (!isTracking()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // Each target corresponds to a depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // Each key corresponds to a dep set
    depsMap.set(key, (dep = createDep()))
  }
  const eventInfo = (process.env.NODE_ENV !== 'production')
    ? { effect: activeEffect, target, type, key }
    : undefined
  trackEffects(dep, eventInfo)
}

function trackEffects(dep, debuggerEventExtraInfo) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      // Mark as new dependency
      dep.n |= trackOpBit 
      // If the dependency has been collected, it does not need to be collected again
      shouldTrack = !wasTracked(dep)
    }
  }
  else {
    // cleanup mode
    shouldTrack = !dep.has(activeEffect)
  }
  if (shouldTrack) {
    // Collect the currently active effect as a dependency
    dep.add(activeEffect)
    // The currently active effect collects dep sets as dependencies
    activeEffect.deps.push(dep)
    if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
      activeEffect.onTrack(Object.assign({
        effect: activeEffect
      }, debuggerEventExtraInfo))
    }
  }
}

We found that when creating a DEP, it is completed by executing the createDep method. In addition, before dep collects the previously activated effect s as dependencies, it will judge whether the dep has been collected. If it has been collected, it does not need to be collected again. In addition, it will be judged whether this dep is a new dependency. If not, it will be marked as new.

Next, let's look at the logic after fn execution:

finally {
  if (effectTrackDepth <= maxMarkerBits) {
    // Complete dependency tag
    finalizeDepMarkers(this)
  }
  // Restore to previous level
  trackOpBit = 1 << --effectTrackDepth
  resetTracking()
  // Out of stack
  effectStack.pop()
  const n = effectStack.length
  // Point to the last effect on the stack
  activeEffect = n > 0 ? effectStack[n - 1] : undefined
}

If the dependency markers are satisfied, you need to execute finalizeDepMarkers to complete the dependency markers. See its implementation:

const finalizeDepMarkers = (effect) => {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i]
      // Dependencies that have been collected but are not new need to be deleted
      if (wasTracked(dep) && !newTracked(dep)) {
        dep.delete(effect)
      }
      else {
        deps[ptr++] = dep
      }
      // Empty status
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

The main task of finalizeDepMarkers is to find those dependencies that have been collected but have not been collected in the new round of dependency collection and remove them from deps. In fact, this is to solve the previously mentioned scenario that requires cleanup: if a responsive object is not accessed during the rendering of a new component, its change should not trigger the re rendering of the component.

The above optimization of dependency collection is realized. It can be seen that compared with the previous process of clearing dependencies and then adding dependencies every time the effect function is executed, the current implementation will mark the dependency status before each function wrapped by the effect, and the collected dependencies will not be collected repeatedly in the process, After executing the effect function, the dependencies that have been collected but have not been collected in the new round of dependency collection will be removed.

After optimization, the operations on dep dependency sets are reduced, which naturally optimizes the performance.

Optimization of responsive API
The optimization of responsive APIs is mainly reflected in the optimization of APIs such as ref and computed.

Take ref API as an example to see its implementation before optimization:

function ref(value) {
  return createRef(value)
}

const convert = (val) => isObject(val) ? reactive(val) : val

function createRef(rawValue, shallow = false) {
  if (isRef(rawValue)) {
    // If a ref is passed in, you can return it and handle the nested Ref.
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl {
  constructor(_rawValue, _shallow = false) {
    this._rawValue = _rawValue
    this._shallow = _shallow
    this.__v_isRef = true
    // In the case of non shallow, if its value is an object or array, the recursive response
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    // Add getter s to the value attribute and collect dependencies
    track(toRaw(this), 'get' /* GET */, 'value')
    return this._value
  }
  set value(newVal) {
    // Add a setter to the value attribute
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // Distribution notice
      trigger(toRaw(this), 'set' /* SET */, 'value', newVal)
    }
  }
}

The ref function returns the return value executed by the createRef function. Within createRef, nested refs are handled first. If the incoming rawValue is also a ref, the rawValue is returned directly; Next, an instance of the RefImpl object is returned.

The internal implementation of RefImpl is mainly to hijack the getter and setter of its instance value attribute.

When accessing the value attribute of a ref object, it will trigger the getter to execute the track function for dependency collection, and then return its value; When modifying the value value of a ref object, it will trigger the setter to set the new value and execute the trigger function to send notifications. If the new value newVal is an object or array type, it will be converted into a reactive object.

Next, let's look at Vue JS 3.2 changes related to the implementation of this part:

class RefImpl {
  constructor(value, _shallow = false) {
    this._shallow = _shallow
    this.dep = undefined
    this.__v_isRef = true
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

The main change is to implement the logic of dependency collection and dispatch notification on the value attribute of ref object.

In Vue In the implementation of ref in JS version 3.2, the dependency collection part is changed from the original track function to trackRefValue. See its implementation:

function trackRefValue(ref) {
  if (isTracking()) {
    ref = toRaw(ref)
    if (!ref.dep) {
      ref.dep = createDep()
    }
    if ((process.env.NODE_ENV !== 'production')) {
      trackEffects(ref.dep, {
        target: ref,
        type: "get" /* GET */,
        key: 'value'
      })
    }
    else {
      trackEffects(ref.dep)
    }
  }
}

You can see that the dependency of ref is directly saved in the dep attribute, while in the implementation of track function, the dependency will be saved in the global targetMap:

let depsMap = targetMap.get(target)
if (!depsMap) {
  // Each target corresponds to a depsMap
  targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
  // Each key corresponds to a dep set
  depsMap.set(key, (dep = createDep()))
}

Obviously, it may be necessary to make multiple judgments and set logic inside the track function, and saving the dependency in the dep attribute of the ref object eliminates this series of judgments and settings, so as to optimize the performance.

Accordingly, for the implementation of ref, the original trigger function is changed to triggerRefValue for the distribution notice. See its implementation:

function triggerRefValue(ref, newVal) {
  ref = toRaw(ref)
  if (ref.dep) {
    if ((process.env.NODE_ENV !== 'production')) {
      triggerEffects(ref.dep, {
        target: ref,
        type: "set" /* SET */,
        key: 'value',
        newValue: newVal
      })
    }
    else {
      triggerEffects(ref.dep)
    }
  }
}

function triggerEffects(dep, debuggerEventExtraInfo) {
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if ((process.env.NODE_ENV !== 'production') && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      }
      else {
        effect.run()
      }
    }
  }
}

Because all its dependencies are obtained directly from the ref attribute and traversed, there is no need to execute some additional logic of the trigger function, so the performance is also improved.

Design of trackOpBit
If you are careful, you may find that the trackOpBit that the tag depends on adopts the left shift operator trackOpBit = 1 < < + + effecttrackdepth in each calculation; And the or operation is used in the assignment:

deps[i].w |= trackOpBit
dep.n |= trackOpBit

So why is it so designed? Because the execution of effect may be recursive, the dependency marking of each level can be recorded in this way.

When judging whether a dep has been collected by dependency, the wasTracked function is used:

const wasTracked = (dep) => (dep.w & trackOpBit) > 0
Copy code
It is judged by whether the result of the and operation is greater than 0, which requires that the nested level of dependencies should be matched when they are collected. For example, if the value of dep.w is 2 at this time, it indicates that it was created when the effect function was executed at the first layer, but the effect function nested at the second layer has been executed at this time, trackOpBit shifts two bits to the left and becomes 4, and the value of 2 & 4 is 0, then the return value of wasTracked function is false, indicating that this dependency needs to be collected. Obviously, this demand is reasonable.

It can be seen that without the design of trackOpBit bit bit operation, it is difficult for you to deal with dependency tags at different nesting levels. This design also reflects the very solid basic computer skills of basvanmeurs.

Topics: Front-end html5 Vue.js