current position:Home>Uncover the responsive optimization of vue.js 3.2!

Uncover the responsive optimization of vue.js 3.2!

2021-08-23 07:46:29 Tong ouba

background

Vue 3 It has been almost a year since it was officially released , I believe that many small partners have been used in the production environment Vue 3 了 . Now ,Vue.js 3.2 It has been officially released , This time, minor The upgrade of the version is mainly reflected in the optimization of the source code level , For the user's use level, it doesn't change much . One of the things that appeals to me is the improved performance of responsiveness :

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

Which translates as ref API The reading efficiency is improved by about 260%, The write efficiency is improved by about 50% , The efficiency improvement of relying on collection is about 40%, At the same time, it also reduces about 17% Memory usage .

This is just an optimization , Because you know, responsive systems are Vue.js One of the core implementations of , Optimizing it means using all Vue.js Developed App Performance optimization .

And this optimization is not Vue Official personnel achieved , But a big man in the community @basvanmeurs Proposed , The relevant optimization code is in 2020 year 10 month 9 No. has been submitted , However, due to the large changes to the internal implementation , The authorities waited until Vue.js 3.2 Release , Just put the code into .

This time, basvanmeurs The proposed responsive performance optimization really makes you happy , Not just greatly improved Vue 3 Run time performance of , And because such core code can come from the contribution of the community , That means Vue 3 Attracted more and more attention ; Some competent developers participate in the contribution of core code , It can make Vue 3 It's better to go farther .

We know , Compared with Vue 2,Vue 3 We've done a lot of optimization , One part is the implementation of data response Object.defineProperty API Changed to Proxy API.

The original Vue 3 When it comes to publicity , Officials claim to have optimized the performance of responsive implementation , So what are the aspects of optimization ? Some small partners think it is Proxy API Better than Object.defineProperty Of , It's not , actually Proxy In terms of performance, it is better than Object.defineProperty Poor , For details, please refer to Thoughts on ES6 Proxies Performance This article , And I tested it , The conclusion is the same as above , You can refer to this repo.

since Proxy slow , Why? Vue 3 Or did you choose it to implement data response ? because Proxy It's essentially a hijacking of an object , In this way, it can not only listen for changes in the value of a property of the object , You can also listen for the addition and deletion of object properties ; and Object.defineProperty Is to add a corresponding to an existing attribute of an object getter and setter, So it can only listen for changes in the value of this attribute , Instead of listening to 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 . stay Vue 2 In the implementation of , When the data is turned into a response in the component initialization phase , When the child attribute is still an object , Will execute recursively Object.defineProperty Define the response of the child object ; And in the Vue 3 In the implementation of , Only when the object attribute is accessed will the type of sub attribute be determined to decide whether to execute recursively reactive, This is actually an implementation of delay definition sub object response , There will be some improvement in performance .

therefore , Compared with Vue 2,Vue 3 Indeed, some optimization has been done in the responsive implementation part , But in fact, the effect is limited . and Vue.js 3.2 This optimization in terms of responsive performance , It really made a qualitative leap , Next, we'll serve some hard dishes , What optimization has been done from the source level , And the technical thinking behind these optimizations .

Principle of responsive implementation

It's called responsive , When we modify the data , Can automatically do something ; The rendering corresponding to the component , After modifying the data , It can automatically trigger the re rendering of components .

Vue 3 Implement responsive , It's essentially through Proxy API Hijacked the reading and writing of data objects , When we access data , Will trigger getter Perform dependency collection ; When modifying data , Will trigger setter Distribution notice .

Next , Let's briefly analyze the implementation of dependency collection and dispatch notifications (Vue.js 3.2 Previous version ).

Rely on collection

First, let's look at the process of dependency collection , The core is when accessing responsive data , Trigger getter function , And then perform track Function collection dependency :

let shouldTrack = true
//  Currently active  effect
let activeEffect
//  Original data object  map
const targetMap = new WeakMap()
function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    //  Every  target  Corresponding to one  depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    //  Every  key  Corresponding to one  dep  aggregate 
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    //  Collect currently active  effect  As a dependency 
    dep.add(activeEffect)
   //  Currently active  effect  collect  dep  Set as dependency 
    activeEffect.deps.push(dep)
  }
}

Before analyzing the implementation of this function , Let's first think about what dependencies to collect , Our goal is to achieve responsive , It can automatically do something when the data changes , For example, execute some functions , So the dependency we collect is the side effect function executed after the data changes .

track The function has three arguments , among target Represents raw data ;type Indicates the type of dependency collection ;key Represents the accessed property .

track A global... Is created outside the function targetMap As the original data object Map, Its key is target, The value is depsMap, As a dependent Map; This depsMap The key is target Of key, The value is dep aggregate ,dep Stored in the collection are dependent side-effect functions . For ease of understanding , The relationship between them can be represented by the following figure :

So every time track function , Is to put the currently activated side effect function activeEffect As a dependency , And collect it target dependent depsMap Corresponding key Dependency set under dep in .

Distribution notice

The dispatch notification occurs at the stage of data update , The core is when modifying responsive data , Trigger setter function , And then perform trigger Function dispatch notification :

const targetMap = new WeakMap()
function trigger(target, type, key) {
  //  adopt  targetMap  Get  target  Corresponding dependency set 
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    //  No dependence , Go straight back to 
    return
  }
  //  Create a running  effects  aggregate 
  const effects = new Set()
  //  add to  effects  Function of 
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }
  // SET | ADD | DELETE  One of the operations , Add 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()
    }
  }
  //  Traverse the execution  effects
  effects.forEach(run)
}

trigger The function has three arguments , among target Represents the target original object ;type Indicates the type of update ;key Indicates the attribute to be modified .

trigger function I did four things :

  1. from targetMap Get in the target Corresponding dependency set depsMap;
  2. Create a running effects aggregate ;
  3. according to key from depsMap Find the corresponding effect Add to effects aggregate ;
  4. Traverse effects Execute the relevant side effect function .

So every time trigger function , It is based on target and key, from targetMap Find all the related side-effect functions in, and traverse and execute them again .

In describing the process of relying on the collection and distribution of notifications , We all mentioned a word : Side effect function , In the process of dependency collection, we put activeEffect( Currently active side effect function ) Collect as dependencies , What is it ? Next, let's look at the true face of the side effect function .

Side effect function

that , What is a side effect function , Before introducing it , Let's first review the original requirements of responsiveness , That is, we can automatically do something by modifying the data , A simple example :

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

We defined responsive objects counter, And then in logCount I visited counter.num, We hope to implement count Function modification counter.num When it's worth it , Can automatically execute logCount function .

According to our previous analysis of the dependency collection process , If logCount yes activeEffect Words , Then the requirements can be realized , But it's obviously impossible , Because the code is executing to console.log(counter.num) In this business , It's on itself logCount The operation in a function is unknown .

So what to do ? In fact, as long as we run logCount Function before , hold logCount Assign a value to activeEffect Just fine :

activeEffect = logCount 
logCount()

Follow this line , We can use the idea of higher-order functions , Yes logCount Make a layer of packaging :

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

wrapper It's also a function , It accepts fn As a parameter , Returns a new function wrapped, Then maintain a global variable activeEffect, When wrapped When it comes to execution , hold activeEffect Set to fn, And then execute fn that will do .

So when we execute wrappedLog after , Go ahead and revise counter.num, Automatically logCount Function .

actually Vue 3 Is to adopt a similar approach , Inside it there is a effect Side effect function , Let's take a look at its implementation :

//  overall situation  effect  Stack 
const effectStack = []
//  Currently active  effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    //  If  fn  Is already a  effect  Function , Then it points to the original function 
    fn = fn.raw
  }
  //  Create a  wrapper, It is a function of reactive side effects 
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // lazy  To configure , Calculating properties will use , Not  lazy  Directly execute once 
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if (!effect.active) {
      //  Inactive state , It is judged that if it is not scheduled to execute , Then execute the original function directly .
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      //  Empty  effect  Referenced dependencies 
      cleanup(effect)
      try {
        //  Turn on the whole  shouldTrack, Allow dependency collection 
        enableTracking()
        //  Pressing stack 
        effectStack.push(effect)
        activeEffect = effect
        //  Execute the original function 
        return fn()
      }
      finally {
        //  Out of the stack 
        effectStack.pop()
        //  recovery  shouldTrack  Status before opening 
        resetTracking()
        //  Point to the last... On the stack  effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++
  //  The logo is a  effect  function 
  effect._isEffect = true
  // effect  The state of oneself 
  effect.active = true
  //  Wrapped primitive function 
  effect.raw = fn
  // effect  Corresponding dependence , bi-directional pointer , Dependency contains a pair of  effect  References to ,effect  It also contains references to dependencies 
  effect.deps = []
  // effect  Related configuration of 
  effect.options = options
  return effect
}

Combined with the above code ,effect Internally through the implementation of createReactiveEffect Function to create a new effect function , In order to communicate with the outside effect Functions distinguish , We call it reactiveEffect function , And added some additional properties to it ( I have marked in my notes ). in addition ,effect The function also supports passing in a configuration parameter to support more feature, It's not going to unfold here .

reactiveEffect A function is a side effect function of a response , When executed trigger When the process sends out the notice , Executive effect Is it .

According to our previous analysis ,reactiveEffect Functions only need to do two things : Let the overall activeEffect Pointing to it , Then execute the wrapped original function fn.

But in fact, its implementation is more complex , First it will judge effect Is the state of active, This is actually a means of control , Allow in non active Status and non scheduling execution , Then execute the original function directly fn And back to .

Then judge effectStack Include in effect, If not, put effect Press into the stack . We mentioned before , Just set activeEffect = effect that will do , So why design a stack structure here ?

In fact, it takes into account the following nesting effect Scene :

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()

Every time we execute effect Function time , If you just put reactiveEffect The function is assigned to activeEffect, So for this nested scenario , After execution effect(logCount2) after ,activeEffect still effect(logCount2) Back to reactiveEffect function , So follow-up visits counter.num When , Dependency collection corresponds to activeEffect That's not right. , At this point, we execute count Function modification counter.num After the execution is not logCount function , It is logCount2 function , The final output is as follows :

num2: 0 
num: 0 
num2: 0

The results we expect should be as follows :

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

So for nested effect Scene , We cannot simply assign values activeEffect, It should be considered that the execution of a function itself is an on and off stack operation , So we can also design a effectStack, So every time I enter reactiveEffect The function puts it on the stack first , then activeEffect Point to this reactiveEffect function , And then fn When the execution is finished, it will be put out of the stack , And then activeEffect Point to effectStack The last element , That is, the outer layer effect Function corresponding reactiveEffect.

Here we also notice a detail , Before the stack, it will execute cleanup Function empty reactiveEffect The dependency corresponding to the function . In execution track Function , In addition to collecting currently active effect As a dependency , And through activeEffect.deps.push(dep) hold dep As activeEffect Dependence , In this way cleanup We can find effect Corresponding dep 了 , And then put effect From these dep Delete in .cleanup The code for the 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 cleanup Well ? If you encounter this kind of scene :

<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>

Combined with the code, you can know , The view of this component will be based on showMsg Variable control display msg Or a random number , When we click Switch View When the button of , Will change the value of this variable .

Suppose there is no cleanup, When rendering the template for the first time ,activeEffect Is the side effect rendering function of the component , Because the template render It's time to visit state.msg, Therefore, dependency collection will be performed , Take the side effect rendering function as state.msg Dependence , We call it render effect. And then we click Switch View Button , The view switches to show random numbers , Now let's click Toggle Msg Button , Because of the modification state.msg Will send a notice , eureka render effect And implement , This triggers the re rendering of the component .

But this behavior does not actually meet expectations , Because when we click Switch View Button , When the view is switched to display random numbers , It will also trigger the re rendering of components , But the view is not rendered at this time state.msg, Therefore, changes to it should not affect the re rendering of components .

Therefore, in the of components render effect Perform before , If you pass cleanup Clean up dependencies , We can delete the previous state.msg Collected render effect rely on . So when we modify state.msg when , Since there are no dependencies, the re rendering of components will not be triggered , In line with expectations .

Optimization of responsive implementation

The principle of responsive implementation is analyzed , Everything seems to be OK, So what other points can be optimized ?

Rely on the optimization of the collection

At present, every time the side effect function executes , You need to do it first cleanup Clear dependency , The dependencies are then collected again during the execution of the side effect function , This process involves a large number of pairs of Set Add and delete operations of collection . In many scenarios , Dependencies rarely change , Therefore, there is a certain optimization space .

In order to reduce the addition and deletion of collections , We need to identify the state of each dependency set , For example, is it a new collection , Or has been collected .

So here we need to set dep Add two properties :

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

among w Indicates whether it has been collected ,n Indicates whether a new collection .

Then design several global variables ,effectTrackDepthtrackOpBitmaxMarkerBits.

among effectTrackDepth Represents recursive nested execution effect The depth of the function ;trackOpBit Used to identify the status of dependency collection ;maxMarkerBits Indicates the number of digits of the maximum tag .

Next, let's look at their applications :

function effect(fn, options) {
  if (fn.effect) {
    fn = fn.effect.fn
  }
  //  establish  _effect  example  
  const _effect = new ReactiveEffect(fn)
  if (options) {
    //  Copy  options  Attributes in to  _effect  in 
    extend(_effect, options)
    if (options.scope)
      // effectScope  Related processing logic 
      recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    //  Execute now 
    _effect.run()
  }
  //  binding  run  function , As  effect runner
  const runner = _effect.run.bind(_effect)
  // runner  Retain the right to  _effect  References to 
  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  rely on 
    this.deps = []
    // effectScope  Related processing logic 
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        //  Pressing stack 
        effectStack.push((activeEffect = this))
        enableTracking()
        //  Record the number of bits according to the recursive depth 
        trackOpBit = 1 << ++effectTrackDepth
        //  exceed  maxMarkerBits  be  trackOpBit  The calculation of will exceed the number of bits of the maximum shaping , Downgrade 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 the previous level 
        trackOpBit = 1 << --effectTrackDepth
        resetTracking()
        //  Out of the stack 
        effectStack.pop()
        const n = effectStack.length
        //  Point to the last... On the stack  effect
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

You can see ,effect The implementation of the function has been modified and adjusted , For internal use ReactiveEffect Class creates a _effect example , And the function returns runner Pointing to ReactiveEffect Class run Method .

That is, execute the side effect function effect Function time , This is what is actually being done run function .

When run Function execution time , We noticed that cleanup Functions no longer execute by default , In encapsulated functions fn Before execution , First, execute trackOpBit = 1 << ++effectTrackDepth Record trackOpBit, Then compare whether the recursion depth exceeds maxMarkerBits, If exceeded ( Usually not ) The old cleanup Logic , If not, execute initDepMarkers Mark dependencies , Look at 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 
    }
  }
}

initDepMarkers Function implementation is simple , Traverse _effect In the instance deps attribute , For each dep Of w Property marked as trackOpBit Value .

And then it will execute fn function , It is the function encapsulated by the side effect function , For example, for component rendering ,fn Is the component rendering function .

When fn When the function is executed , Will access responsive data , Will trigger them getter, And then perform track The function performs dependency collection . Corresponding , The process of dependency collection has also been adjusted :

function track(target, type, key) {
  if (!isTracking()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    //  Every  target  Corresponding to one  depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    //  Every  key  Corresponding to one  dep  aggregate 
    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 dependencies have been collected , There is no need to collect 
      shouldTrack = !wasTracked(dep)
    }
  }
  else {
    // cleanup  Pattern 
    shouldTrack = !dep.has(activeEffect)
  }
  if (shouldTrack) {
    //  Collect currently active  effect  As a dependency 
    dep.add(activeEffect)
    //  Currently active  effect  collect  dep  Set as dependency 
    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 When , It's through execution createDep Method , Besides , stay dep Put the previously activated effect Before collecting as dependencies , Will judge this dep Whether it has been collected , If it has been collected , There is no need to collect again . Besides , This will be judged here dep Is it a new dependency , If not , Mark as new .

Next , Let's see fn Logic after execution :

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

Under the condition that the dependency tag is satisfied , You need to perform finalizeDepMarkers Complete dependency tag , Look at 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]
      //  Once collected but not new dependencies , You need to remove 
      if (wasTracked(dep) && !newTracked(dep)) {
        dep.delete(effect)
      }
      else {
        deps[ptr++] = dep
      }
      //  Empty state 
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

finalizeDepMarkers The main thing to do is to find those dependencies that have been collected but have not been collected in the new round of dependency collection , from deps Remove . This is actually to solve the needs mentioned above cleanup The problem of the scene : Reactive objects not accessed during the rendering of new components , Then its change should not trigger the re rendering of the component .

The above realizes the optimization of dependency collection , You can see that compared with each previous execution effect All functions need to clear dependencies first , Then add the dependent process , The current implementation will be executed every time effect The dependent state is marked before the wrapped function , Dependencies that have been collected during the process will not be collected repeatedly , After execution effect The function also removes the dependencies that have been collected but are not collected in the new round of dependency collection .

After optimization, for dep Operations that depend on collections are reduced , Naturally, the performance is optimized .

Response type API The optimization of the

Response type API The optimization of is mainly reflected in refcomputed etc. API The optimization of the .

With ref API For example , Take a look at 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 the incoming is a  ref, Then return to yourself , Handle nesting  ref  The situation of .
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl {
  constructor(_rawValue, _shallow = false) {
    this._rawValue = _rawValue
    this._shallow = _shallow
    this.__v_isRef = true
    //  Not  shallow  The situation of , If its value is an object or array , Then the recursive response 
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    //  to  value  Attribute addition  getter, And do dependency collection 
    track(toRaw(this), 'get' /* GET */, 'value')
    return this._value
  }
  set value(newVal) {
    //  to  value  Attribute addition  setter
    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)
    }
  }
}

ref Function returned createRef The return value of the function execution , And in the createRef Inside , First, we deal with nested ref The situation of , If the incoming rawValue It's also a ref, Then go straight back rawValue; Then go back to RefImpl Instance of object .

and RefImpl Internal implementation , Mainly an instance of hijacking it value Attribute getter and setter.

When accessing a ref Object's value attribute , Will trigger getter perform track Function does dependency collection and returns its value ; When modifying a ref Object's value value , It triggers setter Set the new value and execute trigger Function dispatch notification , If the new value newVal Is an object or array type , So turn it into a reactive object .

Next , Let's see 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 ref Object's value Property to execute the logic that relies on collecting and dispatching notifications .

stay Vue.js 3.2 Version of ref In the implementation of , About dependency collection , From the original track The function is changed to trackRefValue, Look at 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)
    }
  }
}

As you can see here, just put ref The related dependencies of are saved to dep Properties of the , And in the track Function implementation , Will keep dependencies globally targetMap in :

let depsMap = targetMap.get(target)
if (!depsMap) {
  //  Every  target  Corresponding to one  depsMap
  targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
  //  Every  key  Corresponding to one  dep  aggregate 
  depsMap.set(key, (dep = createDep()))
}

obviously ,track You may need to make multiple judgments and set logic inside the function , And save dependencies to ref Object's dep Attribute omits this series of judgment and settings , To optimize performance .

Corresponding ,ref The implementation of the distribution notice part , From the original trigger The function is changed to triggerRefValue, Look at 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 directly from ref Property gets all its dependencies and traverses the execution , No need to execute trigger Function some additional lookup logic , Therefore, the performance has also been improved .

trackOpBit The design of the

Careful you may find , Tag dependent trackOpBit, The left shift operator is used in each calculation trackOpBit = 1 << ++effectTrackDepth; And when assigning values , The or operation is used :

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

So why is it so designed ? because effect The execution of may be recursive , In this way, you can record the dependency marking of each level .

Judging a dep Whether it has been collected by dependency , Used wasTracked function :

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

Whether the result of the and operation is greater than 0 To judge , This requires that the nested levels of dependencies be matched when they are collected . for instance , Suppose this time dep.w The value of is 2, Explain that it is executed at the first layer effect Function , But at this time, the nested in the second layer has been executed effect function ,trackOpBit Moving two bits to the left becomes 4,2 & 4 The value of is 0, that wasTracked The return value of the function is false, Explain that you need to collect this dependency . obviously , This demand is reasonable .

You can see , without trackOpBit Design of bit operation , It's hard for you to deal with dependency tags at different nesting levels , This design also reflects basvanmeurs The boss has very solid basic computer skills .

summary

Generally in Vue.js The application of , The access and modification of responsive data are very frequent operations , Therefore, the performance optimization of this process , It will greatly improve the performance of the whole application .

Most people go to see it Vue.js Responsive implementation , The most likely goal is to understand the implementation principle , Little attention is paid to whether the implementation is optimal . and basvanmeurs The boss can propose the implementation of this series of optimization , And wrote a benchmark Tools to verify your optimization , It's worth learning .

I hope you will finish reading this article , In addition to praising the third company of forwarding , You can also go and see the original post , Look at their discussion , I believe you will gain more .

The performance optimization of the front end is always a direction worthy of deep exploration , I hope that in the future development , Whether it's writing a framework or a business , You can always think about the possible optimization points .

Reference material

[1] Vue.js 3.2 Upgrade Introduction : https://blog.vuejs.org/posts/vue-3.2.html

[2] basvanmeurs GitHub Address :https://github.com/basvanmeurs

[3] relevant PR Discussion address :https://github.com/vuejs/vue-next/pull/2345

[4] Thoughts on ES6 Proxies Performance: https://thecodebarbarian.com/thoughts-on-es6-proxies-performance

[5] Proxy-vs-DefineProperty repo: https://github.com/ustbhuangyi/Proxy-vs-DefineProperty

[6 ]benchmark Tools : https://github.com/basvanmeurs/vue-next-benchmarks

This article is from WeChat official account. - Front end canteen (webcanteen)

The source and reprint of the original text are detailed in the text , If there is any infringement , Please contact the [email protected] Delete .

Original publication time : 2021-08-18

Participation of this paper Tencent cloud media sharing plan , You are welcome to join us , share .

copyright notice
author[Tong ouba],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2021/08/20210823074626293F.html

Random recommended