preface

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

This is a responsive optimization made by @basvanmeurs, a community guru, in Vue3.2

  1. refThe API read efficiency is improved260%, the improvement of writing efficiency is about50% 。
  2. Depending on the efficiency of collection40%.
  3. Reduced memory footprint17% 。

Seeing this level of improvement, I can only say: I would like to call you the strongest!

How did the big shot do it? Let’s start with Vue3.0.

A, the Proxy

Proxy objects are used to create a Proxy for an object to intercept and customize basic operations (such as property lookup, assignment, enumeration, function calls, and so on)

1, usage,

new Proxy(target, handler)

const origin = {}
const proxy = new Proxy(origin, {
  get(target, key, reciver) {
      console.log(Sayin '! ')
      return target[key]
  },
  set(target, key, value) {
      target[key] = value
      return true
  }
})
proxy.a = 'aaaa'
proxy.a		/ / print sayin!
Copy the code

Students who do not understand the specific usage can go to MDN to see the explanation, or Ruan Yifeng ES6 is also introduced very clearly. Portal ~

Proxy is not fast

Vue3.0’s use of a proxy can give the illusion that it is faster than defineProperty when it is not. This article provides some examples to compare the execution speed of proxy and defineProperty in detail.

This example counts the number of assignments per second via =, defineProperty, and proxy.

You can clearly see in the figure that proxy is not an order of magnitude at all compared to normal assignment and defineProperty.

Why use a proxy if it’s unpleasant?

Because proxy solves the following problems:

  1. In vue2. X, restrictedObject.defineProperty, collect dependencies only throughgetter, triggering dependencies can only be passedsetter. Reactive mode cannot be triggered by adding or deleting properties.
  2. For the same reason, we can’t makeThe Map, the SetThis type of data is converted to reactive,ProxyYou can.
  3. In Vue2. X, for performance purposes,Array responsivity implemented by hijacking native methodsBut throughProxyWe are not going to worry about the empty problem caused by empty Spaces in arrays.

4. Proxy is not a deep Proxy

For deep-level objects, the proxy only proxies the first layer and does not recursively represent each layer of the object.

const origin = {
  data: {
    a: 1.b: 2
  },
  msg: 'message'
}

const handler = {
  get(target, key, reciver) {
    console.log('agents')
    return Reflect.get(target, key, reciver)
  }
}

const proxy = new Proxy(origin, handler)

proxy.msg / / print agent
const data = proxy.data / / print agent

data.a // There is no console.log
Copy the code

As you can see from this example, the proxy proxies both ‘data’ and ‘MSG’, but when it accesses data.a alone, the proxy disappears

New Proxy returns a Proxy object, but proxy.data returns the value of the data attribute in the Origin object, which is no longer a Proxy object.

The implementation of Reactive in Vue is to recursively delegate all objects

Second, the response of Vue3.0

In fact, responsivity in Vue3.0 has the same idea as the original 2.x. In Vue3.0, monorepo is used to couple responsivity as a separate reactivity module.

What followed was a change in some apis, simplified to four important roles:

  1. effect
  2. EffectFunction (fn for short)
  3. Dep
  4. ReactiveData

Let’s talk about it one by one

1, ReactiveData

This is essentially reactive data, done using Object.defineProperty in vue2. x and Proxy in Vue3.0

2, Dep

When Reactive’s GET is triggered, ReactiveData collects dependencies so that the functions collected in the dependencies can be triggered the next time the data changes.

First question: what are the functions that the dependencies collect? React go, others follow: react go, react go, others follow

In addition, unlike 2.x, the previous Dep is stored in the getter by means of closures; In 3.0, Vue manages all DEPs in a unified way through a Map, as shown in figure 3

3, effect

Effect is also mentioned above, which is actually a side effect function. Let’s see how it is used first, and then what does it do

import { reactive, effect } from '@vue/reactivity'
const person = reactive({
  name: 'Itachi'.age: 26
})

const fn = () = > console.log(person.age)

effect(fn)
// print 26

person.age = 27
// print 27

Copy the code

This example gives you a sense of what effect is all about. It’s very simple, or in a colloquial sense, it Bridges reactiveData with functions that use reactiveData. That’s what Effect does.

To do this, we need something other than effect

  1. effectStack: stack is used to hold the current executioneffectBecause theeffectThere are often overlapping calls between.
  2. activeEffect: The top of the stack that is currently executingeffectSo thatReactiveDataMore precise collection of dependencies.

So, effectiFunction is actually the function fn in this example

4, processes,

The specific process is as follows, we fine ~.

5, the source code

// Map raw data to proxy data
const proxyMap = new WeakMap(a){target => key => dep}
const targetMap = new WeakMap(a)// effect executes the stack
const effectStack = []
/ / stack
const activeEffect = null

function reactive(target) {
  // If reactive already exists, go to cache
  if (proxyMap.has(target)) return proxyMap.get(target)
  // The simple type cannot be proxy
  if (typeoftarget ! = ='object') return target

  const proxy = new Proxy(target, {
    get,
    set
  })
  proxy
  return proxy
}

function get(target, key, reciver) {
  const res = Reflect.get(target, key, reciver)
  / / collection
  track(target, key)
  if (isObject(res)) {
    / / recursive proxy
    res = reacitve(res)
  }
  return res
}

function set(target, key, value, reciver) {
  const oldValue = traget[key]
  const res = Reflect.set(target, key, value, reciver)
  / / triggers
  trigger(target, key, value, oldValue)
  return res
}

/ / collection
function track(target, key) {
  // DePS unified management
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}// Collect dependencies
  if (dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

/ / triggers
function trigger(traget, key, value, oldValue) {
  if (value === oldValue) return
  let depsMap = tragetMap.get(target)
  if(! depsMap)return
  let deps = depsMap.get(key)
  if(! deps)return
  deps.forEach(effect= > effect())
}

function effect(fn) {
  const effect = function () {
    // Clear the last cache
    cleanup(effect)
    try {
      effectStack.push(effect)
      activeEffect = effect
      return fn()
    } finally {
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1] | |null
    }
  }
  effect.raw = fn
  effect.deps = []
  return effect
}

// Reset dependencies
function cleanup(effect) {
  const { deps } = effect
  for (let dep of deps) dep.delete(effect)
  deps.length = 0
}

Copy the code

6. Where can I optimize

In fact, we can optimize this cleanup. Every time effect is executed again, we need to cleanup the dependencies collected last time and collect them again. The purpose of this is to avoid dependency collection errors caused by the dependency collection errors caused by the situation that the dependency collected last time is not needed this time

But most scenarios rely on changes that are relatively small, and don’t require such a drastic cleanup and collection.

We can mark old dependencies in advance, mark new dependencies after effect is executed, and compare the old dependencies with the new to determine whether they need to be cleaned up and preserved.

So how can we accurately judge the one-to-many situation in which the same property of ReactiveData may be applied to multiple effects?

Answer: bitmask

3. Bit mask

In addition to + – * /, we have a bit operation.

The bit operators |, &, ~, <<, >>

Assuming we assign meaning to each 1 bit, we can determine whether the current value has the permission by using the bit and ampersand. Component, element, SVG…

const ELEM = 1
const SVG = 1 << 2
const COMPONENT = 1 << 3
const FRAGMENG = 1 << 4
const PORTAL = 1 << 5

function isElement(vnode) {
    return vnode.type & ELEM > 0
}
Copy the code

Iv. How is Vue3.2 optimized

EffectTrackDepth (effectTrackDepth); effectTrackDepth (effectTrackDepth); effectTrackDepth (effectTrackDepth)

Then trackOpBit is used as its bit marker, which can be understood as a unique ID, specifically trackOpBit = 1 << effectTrackDepth.

We also need to transform DEP. The original DEP is just a set, and we add two attributes on this basis to mark which effects the attribute was used in last time and this time, and then delete and add them through comparison.

Since a property of a reactiveData can be used in more than one effect, we can identify which effects the value is used in by bit-marking or marking the DEP, and since the bit-marking of each effect is different, by bit-marking and judging whether the value is greater than zero.

For example

improt { reactive, effect } from '@vue/reactivity'

const data = reacitve({ a: 1 })
effect(() = > {  // effectTrackDepth = 0 trackOpbit = 1 << 0

  console.log(data.a) // data => 'a' => dep.tag |= trackOpbit dep.tag = 1
  
  effect(() = > {  // effectTrackDepth = 1 trackOpbit = 1 << 1
    
    console.log(data.a + 1) // data => 'a' => dep.tag |= trackOpbit dep.tag = 3})})// Dep. Tag & 2 > 0 can be used to determine whether the dep has been used in a particular effect
Copy the code

1. Transform Dep

Dep We only need to add two attributes to the original Set

The original track method also needs to be changed a bit

/ / effect level
const effectTrackDepth = 0
/ / a tag
const trackOpBit = 1

/ / collection
function track(target, key) {
  // DePS unified management
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep = createDep())) }// Collect dependencies
  let shouldTrack = false
  if(! newTracked(dep)) {// Make a new markdep.n |= trackOpBit shouldTrack = ! wasTrack(dep)// No
  }
  if (shouldTrack) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

/ / create the Dep
function createDep() {
  const dep = new Set()
  dep.w = 0 / / the old tags
  dep.n = 0 / / the new tag
  return dep
}
// Check if it was marked before
function wasTracked(dep) {
  return (dep.w & trackOpBit) > 0
}
// Check whether this time is marked
function newTracked(dep) {
  return (dep.n & trackOpBit) > 0
}

Copy the code

2. Transformation effect

Effect We need to do a couple of things

  1. Before effect is executed, effectTrack depth ++ is added
  2. The original collected DEPS are marked with their own tags as old tags
  3. New marks are made to DEP.n through track during execution
  4. End Compare dep.w and dep.n to collate dependencies
  5. effectTrackDepth–

Ok, let’s look at the code

// Check if it was marked before
function wasTracked(dep) {
  return (dep.w & trackOpBit) > 0
}
// Check whether this time is marked
function newTracked() {
  return (dep.n & trackOpBit) > 0
}
function initDepMarkers({ deps }) {
  deps.forEach(dep= > (dep.w |= trackOpBit))
}
function finalizeDepMarkers(effect) {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let dep of deps) {
      if(wasTrack(dep) && ! newTrack(dep)) {// It is not collected this time
        dep.delete(effect)
      } else {
        deps[ptr++] = dep
      }
      // reset in preparation for the next execution
      deps.w &= ~trackOpBit
      deps.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

function effect(fn) {
  const effect = function () {
    try {
      // Mark the effect hierarchy
      trackOpBit = 1 << ++effectTrackDepth
      // Old marks the dependencies collected previously
      initDepMarkers(effect)
      effectStack.push((activeEffect = effect))
      return fn()
    } finally {
      // After executing effect, see which dependencies need to be removed and which dependencies need to be added
      finalizeDepMarkers(effect)
      trackOpBit = 1 << --effectTrackDepth
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1] | |null
    }
  }
  effect.raw = fn
  effect.deps = []
  return effect
}

Copy the code

3. Overall code

// Map raw data to proxy data
const proxyMap = new WeakMap(a){target => key => dep}
const targetMap = new WeakMap(a)// effect executes the stack
const effectStack = []
/ / stack
const activeEffect = null
/ / effect level
const effectTrackDepth = 0
/ / a tag
const trackOpBit = 1

function reactive(target) {
  // If reactive already exists, go to cache
  if (proxyMap.has(target)) return proxyMap.get(target)
  // The simple type cannot be proxy
  if (typeoftarget ! = ='object') return target

  const proxy = new Proxy(target, {
    get,
    set
  })
  proxy
  return proxy
}

function get(target, key, reciver) {
  const res = Reflect.get(target, key, reciver)
  / / collection
  track(target, key)
  if (isObject(res)) {
    / / recursive proxy
    res = reacitve(res)
  }
  return res
}

function set(target, key, value, reciver) {
  const oldValue = traget[key]
  const res = Reflect.set(target, key, value, reciver)
  / / triggers
  trigger(target, key, value, oldValue)
  return res
}

/ / collection
function track(target, key) {
  // DePS unified management
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep = createDep())) }// Collect dependencies
  let shouldTrack = false
  if(! newTracked(dep)) {// Make a new markdep.n |= trackOpBit shouldTrack = ! wasTrack(dep)// No
  }
  if (shouldTrack) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

/ / triggers
function trigger(traget, key, value, oldValue) {
  if (value === oldValue) return
  let depsMap = tragetMap.get(target)
  if(! depsMap)return
  let deps = depsMap.get(key)
  if(! deps)return
  deps.forEach(effect= > effect())
}

/ / create the Dep
function createDep() {
  const dep = new Set()
  dep.w = 0 / / the old tags
  dep.n = 0 / / the new tag
  return dep
}
// Check if it was marked before
function wasTracked(dep) {
  return (dep.w & trackOpBit) > 0
}
// Check whether this time is marked
function newTracked() {
  return (dep.n & trackOpBit) > 0
}
function initDepMarkers({ deps }) {
  deps.forEach(dep= > (dep.w |= trackOpBit))
}
function finalizeDepMarkers(effect) {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let dep of deps) {
      if(wasTrack(dep) && ! newTrack(dep)) {// It is not collected this time
        dep.delete(effect)
      } else {
        deps[ptr++] = dep
      }
      // reset in preparation for the next execution
      deps.w &= ~trackOpBit
      deps.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

function effect(fn) {
  const effect = function () {
    try {
      // Mark the effect hierarchy
      trackOpBit = 1 << ++effectTrackDepth
      // Old marks the dependencies collected previously
      initDepMarkers(effect)
      effectStack.push((activeEffect = effect))
      return fn()
    } finally {
      // After executing effect, see which dependencies need to be removed and which dependencies need to be added
      finalizeDepMarkers(effect)
      trackOpBit = 1 << --effectTrackDepth
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1] | |null
    }
  }
  effect.raw = fn
  effect.deps = []
  return effect
}

Copy the code

4. What else should I pay attention to

So far we’ve done pretty well, but what is it that we haven’t considered, that we have

The bitmask is judged by the number of bits in binary, so is the length of binary infinite?

Apparently not. In JS, “number” is 32 bits, one of which is used as a plus or minus sign, so we only have 31 bits available. What should we do if effect has more than 31 layers?

We’ll just have to do our old cleanup method.

Five, why to see the source code

Last but not least, when I looked at the new VUE code this time, I was actually shocked. As a very ordinary r&d, most of our time was spent in implementing the business of the company.

After I knew that Vue was updated to 3.2, I started to look at the source code of VUe3 for the first time, and I only looked at reactivity. In a short time, Vue updated 7 smaller versions, so that the speed is fast, the speed of reading can not catch up with the speed of updating, the code is changing, you spend half a day to read the code may be iterated one day. So what’s the point of looking?

I’m often asked a dumb question. Can’t you just use VUE? Is it really necessary to spend so much time understanding the source code? So what’s the reason?

In fact, I think most people look at the source of the reason is to test, I started to see vue2. X is the reason.

Of course, there are also a lot of gurus who say that they want to learn design patterns, learn some advanced skills, learn the ideas and apply them to their own projects. When I hear these, I agree with them.

However, I think it is difficult to do this step, and I try to find scenes in my own projects, but I never use the so-called “ideas” I learned in VUE.

So what’s the point of looking at source code for front-end scum like me?

When I saw proxy in Reactive, I actually had an Epiphany. There are a lot of scenarios where we don’t use a lot of native apis, like proxies. I don’t even know how to use it. I don’t understand how it works.

However, when I check MDN and write small examples to test Proxy functions in order to understand Proxy in Reactive, I suddenly feel that these advanced open source frameworks can not only teach you advanced design ideas, but also help you to lay a solid foundation for the front-end.

Source code is like this book, elegant and popular, master can go to learn design mode, small rookie can also lay a solid foundation, slowly into a master.

In this way ~ we encourage each other ~

Vi. Reference materials

  • ProxyMDN
  • The proxy performance
  • Vue3.2 source
  • Vue. Js 3.2 on the optimization of responsive parts — Huang Yidai strongly recommended!!