background

It’s been almost a year since the official release of Vue 3, and I believe many people are already using Vue 3 in production. Today, vue.js 3.2 has been released, and this minor update is mainly about source level optimization, but it doesn’t really change much at the user level. One of the things that appealed to me was the performance of responsiveness:

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

This translates to an increase in read efficiency of approximately 260%, write efficiency of approximately 50%, and dependent collection efficiency of approximately 40%, while reducing memory usage by approximately 17%.

This is a huge optimization, since reactive systems are one of the core implementations of Vue.js, and optimizing them means optimizing the performance of all apps developed using vue.js.

Moreover, this optimization was not implemented by Vue officials, but by @Basvanmeurs, a community leader. The relevant optimization code was submitted on October 9, 2020, but due to a big change to the internal implementation, the official waited until vue.js 3.2 was released. To put the code in.

Basvanmeurs’ responsive performance improvements were a real surprise. Not only did they improve the runtime performance of Vue 3, but the core code was contributed by the community, which meant that Vue 3 was getting more and more attention. With some strong developers involved in the core code contribution, Vue 3 can go a long way.

As we know, compared to Vue 2, Vue 3 has been optimized in many aspects, one part of which is that the data response implementation has been changed from Object. DefineProperty API to Proxy API.

When Vue 3 was initially advertised, it was officially announced that the performance of the responsive implementation had been optimized, so what are the aspects of the optimization? Some people think that the performance of Proxy API is better than that of Object. DefineProperty, but in fact, the performance of Proxy is worse than that of Object. DefineProperty. For details, please refer to Thoughts on ES6 Proxies Performance. I have also tested this and reached the same conclusion. Please refer to the Repo.

If Proxy is slow, why did Vue 3 choose it to implement data response? Because a Proxy is essentially hijacking an object, it can listen not only for changes in the value of a property of the object, but also for additions and deletions of properties. Object.defineproperty adds getters and setters to an existing property of an Object, so it can only listen for changes in the value of the property, not for new or deleted properties.

The performance optimization of responsiveness is actually reflected in turning objects nested at a deeper level into responsive scenarios. In the Vue 2 implementation, when the data is changed to responsive during the component initialization phase, if the child property is still an Object, the Object. DefineProperty defines the responsive of the child Object recursively. In the implementation of Vue 3, only when the object attribute is accessed, the type of the sub-attribute is judged to decide whether to implement reactive recursively. In fact, this is a delayed definition of reactive sub-object implementation, which will improve the performance to some extent.

Therefore, compared with Vue 2, Vue 3 does make some optimization in the responsive implementation, but the effect is actually limited. And vue.js 3.2 in the response performance optimization, is really a qualitative leap, next we will come to some hard dish, from the source code level analysis of specific optimization, as well as the technical level of thinking behind these optimization.

Reactive implementation principle

Reactive means that when we modify the data, we can automatically do something; The rendering of the corresponding component automatically triggers the rerendering of the component after modifying the data.

Vue 3 implements responsiveness, which essentially hijacks reading and writing data objects through the Proxy API. When we access the data, the getter is triggered to perform dependency collection. Setter dispatch notifications are triggered when data is modified.

Next, let’s briefly examine implementations (prior to vue.js 3.2) that rely on collecting and distributing notifications.

Depend on the collection

The core of the dependency collection process is to invoke the getter function when accessing the responsive data, and then execute the track function to collect the dependency:

let shouldTrack = true
// The current active effect
let activeEffect
// The original data object map
const targetMap = new WeakMap(a)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)
  }
}
Copy the code

Before we look at the implementation of this function, let’s think about what dependencies we want to collect. Our goal is to implement responsiveness, which is to automatically do something as the data changes, such as executing certain functions. So the dependencies we collect are side effects that are executed after the data changes.

The track function takes three arguments, where target represents the original data; Type indicates the type of the dependency collection; Key indicates the accessed property.

The track function creates a global targetMap as the Map of the original data object, with the key being target and the value depsMap as the dependent Map. The key of the depsMap is the key of the target, and the value is the deP set, which stores the dependent side effects. For ease of understanding, the relationship between them can be shown in the following figure:

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

Distributed notification

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

const targetMap = new WeakMap(a)function trigger(target, type, key) {
  // Obtain the set of dependencies corresponding to the target from the targetMap
  const depsMap = targetMap.get(target)
  if(! depsMap) {// Return without dependencies
    return
  }
  // Create a running effects collection
  const effects = new Set(a)// Add the effects function
  const add = (effectsToAdd) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        effects.add(effect)
      })
    }
  }
  / / SET | ADD | DELETE operation, one of the corresponding effects
  if(key ! = =void 0) {
    add(depsMap.get(key))
  }
  const run = (effect) = > {
    // Schedule execution
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    }
    else {
      // Run directly
      effect()
    }
  }
  // Iterate over the effects
  effects.forEach(run)
}
Copy the code

The trigger function takes three arguments, where target represents the original object to be targeted; Type indicates the type of update. Key indicates the attribute to be modified.

The trigger function does four main things:

  1. Select * from targetMap; select * from targetMap;

  2. Create a running effects collection;

  3. Select effect from depsMap by key and add it to effects set;

  4. Iterating effects executes the associated side effect functions.

So each time you execute the trigger function, you iterate through the targetMap to find all the associated side effects based on the target and key.

In the process of describing dependency collection and dispatch notifications, we both mentioned the word side effect function. In dependency collection, we refer to the activeEffect function as a dependency collection. What is it? Let’s take a look at the side effect function.

Side effect function

So, what is a side effect function? Before introducing it, let’s go back to the original requirement of responsiveness, which is that we change the data to automatically do something. Here’s a simple example:

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

We defined the responsive object counter, and then we accessed counter. Num in logCount. We wanted the logCount function to be automatically executed when the count function was executed to change the value of counter.

Based on our previous analysis of the dependency collection process, if logCount were an activeEffect, this would be possible, but it is not possible because when the code is executed on the console.log(counter.num) line, It doesn’t know anything about its run in the logCount function.

So what to do? Before we run the logCount function, we assign the value of logCount to the activeEffect:

activeEffect = logCount 
logCount()
Copy the code

With this in mind, we can use the idea of higher-order functions to encapsulate logCount:

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

Wrapper itself is a function that accepts fn as an argument, returns a new function wrapped, and maintains a global variable Called activeEffect. When wrapped executes, set activeEffect to fn and execute fn.

After wrappedLog is executed and counter. Num is changed, the logCount function is automatically executed.

Vue 3 takes a similar approach and has an effect function inside it. Let’s look at its implementation:

// The global effect stack
const effectStack = []
// The current active effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    // If fn is already an effect function, it refers to the original function
    fn = fn.raw
  }
  // Create a wrapper, which is a function that responds to side effects
  const effect = createReactiveEffect(fn, options)
  if(! options.lazy) {// In the lazy configuration, the calculated property is used. In the non-lazy configuration, the property is executed once
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if(! effect.active) {// If the function is not scheduled, then the original function is executed directly.
      return options.scheduler ? undefined : fn()
    }
    if(! effectStack.includes(effect)) {// Clear the dependencies of the effect reference
      cleanup(effect)
      try {
        // Enable global shouldTrack to allow dependency collection
        enableTracking()
        / / pressure stack
        effectStack.push(effect)
        activeEffect = effect
        // Execute the original function
        return fn()
      }
      finally {
        / / out of the stack
        effectStack.pop()
        // Restore the state when shouldTrack was enabled
        resetTracking()
        // Point to the last effect on the stack
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++
  // The id is an effect function
  effect._isEffect = true
  // Effect state
  effect.active = true
  // Wrap the original function
  effect.raw = fn
  // The dependency is a bidirectional pointer that contains a reference to effect, and effect contains a reference to the dependency
  effect.deps = []
  // Configure effect
  effect.options = options
  return effect
}
Copy the code

In combination with the above code, effect internally creates a new effect by executing the createReactiveEffect function. To distinguish it from the external effect function, we call it reactiveEffect. I’ve also added some additional attributes (which I’ve noted in the comments). In addition, the effect function supports passing in a configuration parameter to support more features, which I won’t expand here.

The reactiveEffect function is the reactive side effect function that is executed when the trigger process dispatches the notification.

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.

In fact, it’s a little more complicated to implement. First, it determines whether effect’s state is active, which is actually a control that allows the original function fn to be executed and returned in non-active state and unscheduled execution.

We then determine if effect is included in the effectStack, and if it is not, we push it onto the stack. As mentioned earlier, all you need to do is set activeEffect = effect, so why design a stack structure?

Consider the following nested effect scenario:

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()
Copy the code

If we only assign reactiveEffect to activeEffect every time we execute effect, then for this nested scenario, after effect(logCount2), The activeEffect function is returned by effect(logCount2), so that subsequent visits to counter. Num rely on collecting the corresponding activeEffect function. Instead of executing logCount, we will execute logCount2, and the final output will be as follows:

num2: 0 
num: 0 
num2: 0
Copy the code

What we should expect is the following:

num2: 0 
num: 0 
num2: 0 
num: 1
Copy the code

So for the nested effect scenario, instead of simply assigning the value activeEffect, we should consider that the execution of the function itself is a push-off operation, so we can also design an effectStack so that every time we enter the reactiveEffect function we push it first. And then activeEffect points to this reactiveEffect function, and then after fn executes, it goes off the stack, and then activeEffect points to the last element of the effectStack, Which is the reactiveEffect corresponding to the outer effect function.

Another detail we notice here is that a cleanup function is executed to clear the reactiveEffect dependencies before pushing the stack. When the track function is executed, in addition to collecting the currently activeEffect as a dependency, the activeeffect.deps.push (dep) is used to make dep as a dependency on the activeEffect. In this way, we can find the deP of effect at cleanup and remove effect from the deP. The code for the cleanup function looks like this:

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

Why 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>Copy the code

This component’s View will display MSG or a random number based on the control of the showMsg variable, which will be changed when we click the Switch View button.

If there is no cleanup, activeEffect is the component’s side effect function when rendering the template for the first time. Because the template render accesses state.msg, the dependency collection will be performed, and the side effect function will be the dependency of state.msg. We call it the Render effect. Then we click the Switch View button, the View is switched to display random number, at this time we click the Toggle Msg button, because the state. Msg is modified, it will send a notification, find the Render effect and execute, it will trigger the component to re-render.

This behavior is actually not as expected, because when we click the Switch View button and the View switches to display random numbers, it also triggers a rerendering of the component, but the View does not render state. MSG at this time, so changes to it should not affect the rerendering of the component.

So if we cleanup dependencies by cleanup before the component’s render effect is executed, we can remove the render effect dependencies collected by state.msg. This way when we modify state. MSG, the component will not be rerendered because there are no dependencies, as expected.

Optimization of reactive implementation

After analyzing the responsive implementation principle, everything seems to be OK, so what else can be optimized?

Optimizations that rely on collection

Currently, every side effect function execution requires a cleanup of dependencies and then a re-collection of dependencies during side effect execution, which involves a lot of adding and removing of sets. In many scenarios, dependencies are rarely changed, so there is room for optimization.

To reduce collection additions and deletions, we need to identify the state of each dependent collection, such as whether it is newly collected or has already been collected.

So here we need to add two properties to the collection DEp:

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

W indicates whether the collection has been made, and n indicates whether the collection is new.

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

Where effectTrackDepth represents the depth of recursively nested execution of effect function; TrackOpBit is used to identify the status of dependency collection; MaxMarkerBits Indicates the number of bits of the maximum token.

Here’s how they work:

function effect(fn, options) {
  if (fn.effect) {
    fn = fn.effect.fn
  }
  // Create the _effect instance
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // Copy the properties from options to _effect
    extend(_effect, options)
    if (options.scope)
      // effectScope related processing logic
      recordEffectScope(_effect, options.scope)
  }
  if(! options || ! options.lazy) {// Execute immediately
    _effect.run()
  }
  // Bind the run function as the effect Runner
  const runner = _effect.run.bind(_effect)
  // Keep the reference to _effect in runner
  runner.effect = _effect
  return runner
}

class ReactiveEffect {
  constructor(fn, scheduler = null, scope) {
    this.fn = fn
    this.scheduler = scheduler
    this.active = true
    // Effect stores the associated DEPS dependencies
    this.deps = []
    // effectScope related processing logic
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      return this.fn()
    }
    if(! effectStack.includes(this)) {
      try {
        / / pressure stack
        effectStack.push((activeEffect = this))
        enableTracking()
        // Record bits according to the depth of the recursion
        trackOpBit = 1 << ++effectTrackDepth
        // If maxMarkerBits is exceeded, the trackOpBit computation will exceed the maximum integer number of digits, and demote to cleanupEffect
        if (effectTrackDepth <= maxMarkerBits) {
          // Mark dependencies
          initDepMarkers(this)}else {
          cleanupEffect(this)}return this.fn()
      }
      finally {
        if (effectTrackDepth <= maxMarkerBits) {
          // Finish the dependency tag
          finalizeDepMarkers(this)}// Restore to the upper level
        trackOpBit = 1 << --effectTrackDepth
        resetTracking()
        / / out of the 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}}}Copy the code

As you can see, the effect implementation has been modified to create an instance of _effect internally using the ReactiveEffect class, and the runner returned by the function refers to the run method of the ReactiveEffect class.

When the side 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. TrackOpBit = 1 << ++effectTrackDepth is executed first before the fn function is executed. If it does (which it usually does not) then the old cleanup logic continues. If it does not run initDepMarkers to mark the dependencies and see how it works:

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

The initDepMarkers function is implemented simply by traversing the DEps attributes in the _effect instance, marking each DEP’s W attribute to the trackOpBit value.

The next step is to execute the fn function, which is the function encapsulated by the side effect function, such as for component rendering, fn is the component rendering function.

When the fn function executes, the reactive data is accessed, and their getters are triggered, and the track function executes the dependency collection. Accordingly, some adjustments have been made to the process of relying on 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()))
  }
  consteventInfo = (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 a new dependency
      dep.n |= trackOpBit 
      // If dependencies have already been collected, there is no need to collect them againshouldTrack = ! wasTracked(dep) } }else {
    / / the cleanup modeshouldTrack = ! 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))
    }
  }
}
Copy the code

The createDep method is used to create the dep. In addition, the dep will determine whether the deP has been collected before the dep collects the previously activated effect as a dependency. If the deP has been collected, it does not need to be collected again. In addition, it will determine if the DEP is a new dependency, and if it is not, it will be marked as new.

Next, let’s look at the logic after fn is executed:

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

FinalizeDepMarkers is used to complete the dependency tag if the dependency tag is satisfied. To see how it works, run finalizeDepMarkers:

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 removed
      if(wasTracked(dep) && ! newTracked(dep)) { dep.delete(effect) }else {
        deps[ptr++] = dep
      }
      // Clear the state
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}
Copy the code

The main thing finalizeDepMarkers does is identify dependencies that have been collected but are not collected ina new round of dependency collections and remove them from DEPS. This is actually the scenario to solve the aforementioned cleanup: a responsive object that is not accessed during the new component rendering should not trigger a rerendering of the component.

This is the optimization of dependency collection. You can see that compared with the previous process of clearing dependencies and adding dependencies each time the effect function is executed, the implementation will now mark the state of dependencies before each execution of the effect function. In the process, the collected dependencies will not be collected again. Executing effect also removes dependencies that have been collected but are not collected in a new round of dependency collection.

The optimization results in fewer operations on the set of DEP dependencies, which results in better performance.

Optimization of responsive APIS

The optimization of responsive APIS is mainly reflected in the optimization of REF and computed apis.

Take the REF API as an example, and look at its pre-optimized implementation:

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, then return itself to handle the nested ref case.
    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 non-shallow case, if its value is an object or an array, then the response is recursive
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    // Add a getter to the value property and do a dependency collection
    track(toRaw(this), 'get' /* GET */.'value')
    return this._value
  }
  set value(newVal) {
    // Add a setter for the value property
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // Distribute notices
      trigger(toRaw(this), 'set' /* SET */.'value', newVal)
    }
  }
}
Copy the code

The ref function returns the value that createRef executes. Inside createRef, it first handles the nesting of the ref. If the rawValue passed in is also a ref, then it returns the rawValue. The instance of the RefImpl object is then returned.

RefImpl’s internal implementation, on the other hand, hijacks the getters and setters of its instance value property.

When accessing the value property of a ref object, it triggers the getter to perform the track function to collect dependencies and return its value; When changing the value of a ref object, the setter is triggered to set the new value and the trigger function is executed to send the notification. If the new value newVal is an object or array type, it is converted to a reactive object.

Next, let’s look at the implementation changes in vue.js 3.2:

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)
    }
  }
}
Copy the code

The main change is the logic for performing dependency collection and distribution notifications on the value attribute of the REF object.

The track function is changed to trackRefValue for dependency collection in vue.js 3.2 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)
    }
  }
}
Copy the code

You can see that the dependencies of ref are directly stored in the DEp attribute, whereas in the track implementation, the dependencies are stored 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()))
}
Copy the code

Obviously, the track function may need to do a lot of judgment and setup logic internally, and saving the dependencies to the DEP attribute of the REF object eliminates this series of judgment and setup, thus optimizing performance.

Accordingly, the ref implementation has changed the triggerRefValue function from the original trigger function to the notification part of the distribution. Here’s the 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()
      }
    }
  }
}
Copy the code

There is also a performance boost because it picks up all of its dependencies directly from the ref attribute and iterates through the execution, without needing to execute the trigger function’s extra logic.

The design of the trackOpBit

If you are careful, you may notice that the trackOpBit that marks the dependency is calculated each time using the left-shifted operator trackOpBit = 1 << ++effectTrackDepth; And in the assignment, the or operation is used:

deps[i].w |= trackOpBit
dep.n |= trackOpBit
Copy the code

So why do they do this? Since effect’s execution can be recursive, this is the way to record the dependency markers at each level.

To determine if a deP has been collected by a dependency, use the wasTracked function:

const wasTracked = (dep) = > (dep.w & trackOpBit) > 0
Copy the code

This is determined by whether the result of the and operation is greater than zero, which requires that the nested hierarchy match when the dependency is collected. For example, if dep.w has a value of 2, which means it was created when effect was executed at the first level, but effect nested at the second level has already been executed, the trackOpBit moves left by two digits to 4, and the value of 2&4 is 0, The wasTracked function returns false, indicating that the dependency needs to be collected. Clearly, this is a reasonable requirement.

As you can see, without the trackOpBit bit operation design, it is difficult to deal with the dependency tags at different nested levels. This design also shows the basvanmeurs’ very solid computer foundation.

conclusion

In vue. js applications, reactive data is frequently accessed and modified. Therefore, optimizing the performance of this process will greatly improve the performance of the entire application.

Most people look at the vue. js responsive implementation, probably for the purpose of understanding the implementation principle, and rarely pay attention to whether the implementation is optimal. Basvanmeurs proposed the implementation of this series of optimizations and hand-wrote a Benchmark tool to verify their optimization, which is worth learning from.

I hope that after reading this article, in addition to liking, favoriting and forwarding the three links, you can also check out the original post and their discussion, I believe you will gain more.

Front-end performance optimization is always a good place to dig deep, and hopefully you’ll always be able to think about possible optimization points in future development, whether you’re writing frameworks or businesses.