Because Vue 3.0 was just released, I was going to have a brief understanding of it, but after learning about the new features of Vue 3.0, such as Monorepo code management, source code biased toward functional programming, and Composition Api design, I applauded and decided to study it seriously. I bought Vue 3.0 source code analysis at a cheap price, and compiled a note on Vue 3.0 responsive principle based on the recent hands-on Api learning in the pull pull front-end training camp.

The agent

Before you dive into Vue 3.0, you must first understand Javascript proxies, because Vue 3.0’s reactive data is proxy-based. In the Vue 2.0 era, responsive data was implemented based on Object.defineProperty, which can be summarized as follows:

Object. DefineProperty has the advantage of good compatibility and can control the details of Object attributes. However, there are also corresponding problems. Fourth, you can’t handle arrays.

The above problems are solved by Proxy in Vue 3.0.

The proxy pattern

The Proxy is not a unique object of JS. JS Proxy is designed based on the Proxy mode, so understanding the Proxy mode helps us better understand the Proxy. The proxy pattern refers to the design of indirectly manipulated objects, as summarized in a diagram:

The shortcut we usually use on the desktop is actually the implementation of proxy mode, the user does not open the application directly, but through the shortcut of the desktop.

Proxy

Javascript proxies are designed based on the above Proxy pattern and can be used to manipulate objects or arrays indirectly.

Here’s a quick code example to illustrate its use:

const target = { foo: 'bar' }; const handler = { get() { return 'handler override'; }}; const proxy = new Proxy(target, handler); console.log (proxy.foo) // handler override console.log (target.foo) // barCopy the code

Proxy receives two parameters, the first is the object to be Proxy, the second is the handler, it is an object, there are Proxy specified capture methods, such as GET, set, delete, used to operate the Proxy object, trigger different capture. Note that unlike Object.defineProperty, it is based on the entire Object rather than attributes. Users can operate the instance created by Proxy to indirectly operate the object itself. In the example above, add a GET handler that overloads fetching objects.

Get Receives three parameters: trapTarget, Property, and receiver. TrapTarget is the capture object, Property is the property, and Receiver is the proxy object itself. With these parameters, you can reconstruct the original behavior of the captured method:

const target = {
foo: 'bar'
};

const handler = {
get(trapTarget, property, receiver) {
  console.log (receiver === proxy) // true
returntrapTarget[property]; }};const proxy = new Proxy(target, handler);
console.log(proxy.foo); // true bar
console.log(target.foo); // bar
Copy the code

All methods that can be captured in a handler object have corresponding Reflection API methods. These methods have the same name and function signature as the methods intercepted by the catcher, and they also have the same behavior as the intercepted methods. Therefore, it is also possible to define empty proxy objects using the reflection API as follows:

const target = {
  foo: 'bar'
};

const handler = {
  get() {
    return Reflect.get(...arguments);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
Copy the code

So much for a brief introduction to Proxy, see MSDN or Javascript Advanced Programming (4th edition) for details.

Reactive simple implementation

The creation of reactive data in Vue 2.0 takes place in a “black box”, where reactive data is created based on the parameters passed in when a Vue instance is created. In Vue 3.0, you can explicitly create responsive data:

<template>
  <div>
    <p>{{ state.msg }}</p>
    <button @click="random">Random msg</button>
  </div>
</template>
<script>
  import { reactive } from 'vue'
  export default {
    setup() {
      const state = reactive({
        msg: 'msg reactive'
      })

      const random = function() {
        state.msg = Math.random()
      }

      return {
        random,
        state
      }
    }
  }
</script>
Copy the code

The above example imports the reactive function to explicitly create reactive data.

Before reading the reactive source code, implement a simple version of Reactive to understand it.

Check out the official documentation about what Reactive does:

Returns a reactive copy of the object.

The Reactive conversion is “deep” — it affects all properties. In The ES2015 Proxy Based implementation, the returned proxy is not equal to the original object. It is recommended to work exclusively with the reactive proxy and avoid relying on the original object.

In simple terms, Reactive receives an object and returns a reactive copy, which is a Proxy instance in which properties, including nested objects, are reactive.

The function first checks whether the parameter is an object and returns a Proxy instance:

// Because the null typeof is also an object, we need to add additional criteria for it
const isObject = val= >val ! = =null && typeof val === 'object'

function reactive (target) {
  if(! isObject (target)) {return target
  }
  ...
  return new Proxy(target, handler)
}
Copy the code

Now that you can implement the trap, you need to be careful how you handle nested cases:

const isObject = val => val ! == null && typeof val === 'object' const convert = target => isObject(target) ? reactive(target) : target function reactive (target) { if (! isObject(target)) return target const handler = { get (target, key, receiver) { const result = Reflect.get(target, key, receiver) return convert(result) }, set (target, key, value, receiver) { const oldValue = Reflect.get(target, key, receiver) let result = true if (oldValue ! == value) { result = Reflect.set(target, key, value, receiver) } return result }, deleteProperty (target, key) { const result = Reflect.deleteProperty(target, key) return result } return new Proxy(target, handler) }Copy the code

In the case of nested objects, Vue 2.0 directly recursively transforms them into responsive data when creating instances, while Vue 3.0 deals with them when obtaining corresponding attributes to determine whether they are nested objects, and recursively creates responsive data to optimize performance.

There is a rough implementation, but the most critical responsive part is not yet implemented. The responsive design of Vue 3.0 is similar to Vue 2.0 and also uses the observer pattern, so a simple implementation of Vue 2.0 can be referred to to help you understand the implementation of Vue 3.0.

Vue 3.0 will have a global TargetMap for collecting dependencies with keys for the dependent object and values for the Map, keys for the dependent attribute, and values for the function that needs to be called when the attribute changes. So we need track and trigger functions, the former to collect dependencies, and the latter to call functions when properties change.

let targetMap = new WeakMap(a)// global variables to store dependencies

function track (target, key) {
  if(! activeEffect)return
  let depsMap = targetMap.get(target)  // Get the Map of the dependent object
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key) // Get the function to be called according to the object properties, create if there is no
  if(! dep) { depsMap.set(key, (dep =new Set()))
  }
  dep.add(activeEffect)
}

function trigger (target, key) {
  const depsMap = targetMap.get(target)
  if(! depsMap)return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect= > {
      effect()
    })
  }
}
Copy the code

The most critical effects remain to be implemented. Before implementing it, let’s look at a usage example:

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import { reactive, effect } from './reactivity/index.js'

    const product = reactive({
      name: 'iPhone'.price: 5000.count: 3
    })
    let total = 0 
    effect(() = > {
      total = product.price * product.count
    })
    console.log(total)  / / 15000

    product.price = 4000
    console.log(total) / / 12000

    product.count = 1
    console.log(total) / / 4000

  </script>
</body>
</html>
Copy the code

As you can see from the above example, when the effect function is called, it executes the function passed in once, and if the value in the function passed in later changes, it calls the function passed in earlier. The question is how does Vue know which function to call? The answer is that when the effect function is called, Vue already collects the dependency when it gets the corresponding value. Let’s look at the implementation of effect:

let activeEffect = null  // The global pointer points to the function that was recently passed effect
function effect (callback) {
  activeEffect = callback
  callback() // Access responsive object properties to collect dependencies
  activeEffect = null
}
Copy the code

Use the above example to explain the effect execution process. If we call effect, activeEffect refers to totalSum. If we call totalSum, it will get product.price and product.count, respectively. The get trap for the proxy object is fired, and therefore the track function is fired to collect dependencies. Watch Track again:

let targetMap = new WeakMap(a)// global variables to store dependencies

function track (target, key) {
  if(! activeEffect)return
  let depsMap = targetMap.get(target)  // Get the Map of the dependent object
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key) // Get the function to be called according to the object properties, create if there is no
  if(! dep) { depsMap.set(key, (dep =new Set()))
  }
  dep.add(activeEffect)  // Collect dependencies
}
Copy the code

Add track and trigger to the proxy object to reactive:

const isObject = val= >val ! = =null && typeof val === 'object'
const convert = target= > isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) = > hasOwnProperty.call(target, key)

function reactive (target) {
  if(! isObject(target))return target

  const handler = {
    get (target, key, receiver) {
      // Collect dependencies
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      return convert(result)
    },
    set (target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if(oldValue ! == value) { result =Reflect.set(target, key, value, receiver)
        // Trigger the update
        trigger(target, key)
      }
      return result
    },
    deleteProperty (target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        // Trigger the update
        trigger(target, key)
      }
      return result
    }
  }

  return new Proxy(target, handler)
}
Copy the code

Vue 3.0’s responsivity principle is summarized using a diagram found on the web:

The source code to read

Once you have the basics of a simple implementation, you can read the source code.

Reactive function source code in the source path packages/reactivity/SRC/Reactive. Ts.

function reactive (target) {
   // If you try to make a readonly proxy responsive, return the readonly proxy directly
  if (target && target.__v_isReadonly) {
     return target
  } 

  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}

// isReadonly specifies whether the target is read-only, baseHandlers are the proxy catcher for the base data type, and collectionHandlers are the collection
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {

  if(! isObject(target)) {// The target must be an object or array type
    if((process.env.NODE_ENV ! = ='production')) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }

  if(target.__v_raw && ! (isReadonly && target.__v_isReactive)) {// Target is already a Proxy object
    // With one exception, continue if readOnly is applied to a responsive object
    return target

  }

  if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {

    // Target already has a Proxy
    return isReadonly ? target.__v_readonly : target.__v_reactive
  }

  // Only whitelisted data types can become responsive

  if(! canObserve(target)) {return target
  }

  // Use Proxy to create reactive style
  const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)

  // Mark the raw data to indicate that it has become responsive and has a corresponding Proxy
  def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)

  return observed
}
Copy the code

This is the basic build process of Reactive, which is similar to the previous implementation, but with more source code consideration. IsReadonly is a Boolean value that indicates whether the proxy object is read-only. One advantage of Proxy over Object.defineProperty is that it can handle arrays.

The canObserve function further restricts target:

const canObserve = (value) = > {
  return(! value.__v_skip && isObservableType(toRawType(value)) && !Object.isFrozen(value))
}

const isObservableType = /*#__PURE__*/ makeMap('Object,Array,Map,Set,WeakMap,WeakSet')
Copy the code

Objects with a __v_SKIP attribute, frozen objects, and objects not in the whitelist cannot become responsive.

const observed = new Proxy(target, collectionTypes.has(target.constructor) ? Creates the proxy object and, according to target’s constructor, returns baseHandlers, whose value is mutableHandlers, if it is a basic data type.

const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
Copy the code

Whichever processor function is hit, it does one of two things: collecting and distributing notifications.

Dependency collection: the GET function

Take a look at the source code for creating the GET trap:

function createGetter(isReadonly = false) {

  return function get(target, key, receiver) {
    // Returns different results according to different attributes
    if (key === "__v_isReactive" /* isReactive */) {
      / / agent observed. __v_isReactive
      return! isReadonly }else if (key === "__v_isReadonly" /* isReadonly */) {
      / / agent observed. __v_isReadonly
      return isReadonly;
    }
    else if (key === "__v_raw" /* raw */) {
      / / agent observed. __v_raw
      return target
    }

    const targetIsArray = isArray(target) 
    
    // If target is an array and the attribute is contained in arrayInstrumentations
    ArrayInstrumentations contains functions that modify some methods of the array
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    / / evaluated
    const res = Reflect.get(target, key, receiver)

    // The built-in Symbol key does not rely on collection
    if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
      return res
    }

    // Rely on collection! isReadonly && track(target,"get" /* GET */, key)
    
    return isObject(res)
      ? isReadonly
        ?
        readonly(res)
        // If res is an object or array type, execute reactive recursively to make res reactive
        : reactive(res)
      : res
  }
}
Copy the code

Look at arrayInstrumentations, which is a collection of dependencies on an array of proxy methods that call the included methods:

const arrayInstrumentations = {}
['includes'.'indexOf'.'lastIndexOf'].forEach(key= > {
  arrayInstrumentations[key] = function (. args) {
    // toRaw can convert reactive objects into raw data
    const arr = toRaw(this) // This is the array itself
    for (let i = 0, l = this.length; i < l; i++) {
      // Rely on collection
      track(arr, "get" /* GET */, i + ' ')}// Try using the parameters themselves, possibly reactive data
    constres = arr[key](... args)if (res === -1 || res === false) {
      // If this fails, try again to convert the parameter to the original data
      returnarr[key](... args.map(toRaw)) }else {
      return res
    }
  }
})
Copy the code

Why change these methods? Because these methods may get different values when modifying array data, they need to be recollected each time they are called.

Before looking at the track function, take a look at the end of the get function, and finally take different actions based on the result. If it is a basic data type, return the value directly, otherwise the recursion becomes reactive data, which is different from Vue 2.0. Vue 2.0 is the direct recursive processing when creating, while vUE 3 is the judgment whether to process or not when obtaining the attribute, and the delayed definition of the responsive implementation of sub-objects will have a great improvement in performance.

Finally, let’s look at the core function track of GET:

// Whether dependencies should be collected
let shouldTrack = true

// Effect currently active
let activeEffect

// Raw 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 effects as dependencies
    dep.add(activeEffect)
   // The currently active effect collects the DEP collection as a dependency
    activeEffect.deps.push(dep)
  }
}
Copy the code

The basic implementation is the same as the previous simple version, except that effects that are now activated also collect THE DEP as a dependency.

Sending notification: set function

Sending notifications occurs during the data update phase, and since we hijack the data object using the Proxy API, the set function is executed when the reactive object properties are updated. Let’s look at the implementation of the set function, which returns the createSetter function:

function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    value = toRaw(value)
    const hadKey = hasOwn(target, key)
    // Use Reflect to modify
    const result = Reflect.set(target, key, value, receiver)

    // If the target's prototype chain is also a proxy, modifying the attributes on the prototype chain via reflect.set will trigger the setter again, in which case there is no need to trigger twice
    if (target === toRaw(receiver)) {
      if(! HadKey) {increment without attribute triggers increment type trigger(target,"add" /* ADD */, key, value)
      }
      else if(hasChanged(value, oldValue)) {trigger(target,"set" /* SET */, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

The logic of the set is simple, with the emphasis on the trigger function, which sends out notifications.


WeakMap is characterized by a key that is a reference
const targetMap = new WeakMap(a)function trigger(target, type, key, newValue) {
  // Get the target dependency set from targetMap
  const depsMap = targetMap.get(target)

  if(! depsMap) {// No dependencies, return directly
    return
  }

  // Create a collection of effects to run
  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 effects
  effects.forEach(run)
}
Copy the code

Dispatching notifications is similar to the previous implementation, which takes the corresponding proxy object property collection dependency and then dispatches notifications.

Side effect function: effect

The main focus is on ActiveEffects (the current activation side effect function), which is much more complex to implement than the previous lite version and is the focus of the overall Vue 3.0 responsiveness.

// Global effect stack
const effectStack = []

// Effect currently active
let activeEffect

function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    // If fn is already an effect function, point to the original function
    fn = fn.raw
  }

  // Create a wrapper to wrap the incoming function, which is a reactive side effect function
  const effect = createReactiveEffect(fn, options)

  if(! options.lazy) {// The lazy configuration is used to calculate attributes, and the non-lazy configuration is executed directly once
    effect()
  }

  return effect
}
Copy the code

Instead of simply pointing to the most recently used effect function, the source code also wraps the effect function. See how it wraps:

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect(. args) {
    if(! effect.active) {// The original function is executed directly if the execution is not scheduled.
      return options.scheduler ? undefined: fn(... args) }if(! effectStack.includes(effect)) {// Clear the dependencies referenced by effect
      cleanup(effect)

      try {
        // Enable global shouldTrack to allow dependency collection
        enableTracking()
        
        / / pressure stack
        effectStack.push(effect)
        activeEffect = effect
        
        // Execute the original function
        returnfn(... args) }finally {
        / / out of the stack
        effectStack.pop()

        // Restore the state before shouldTrack was started
        resetTracking()

        // point to the last effect on the stack
        activeEffect = effectStack[effectStack.length - 1]}}}// Give effect an ID
  effect.id = uid++

  // The identifier is an effect function
  effect._isEffect = true

  // effect state of itself
  effect.active = true

  // Wrap the original function
  effect.raw = fn

  // Effect dependencies, bidirectional Pointers, dependencies contain references to effect, and effect also contains references to dependencies
  effect.deps = []

  // effect configuration
  effect.options = options

  return effect
}
Copy the code

CreateReactiveEffect returns an effect function with attributes. Wrapping the effect function does two things:

  1. Set the direction of activeEffect
  2. In and out of the effectStack

Point 1 was in the easy version before, why have an effectStack? Because you’re dealing with nested scenarios, consider the following scenarios:

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

We want logCount to be triggered when counter. Num changes, but if there is no stack, there is only an activeEffect pointer. When effect(logCount) is called, the activeEffect pointer points to logCount2, Instead of logCount, so the final result is:

num2: 0 

num: 0 

num2: 0
Copy the code

Instead of:

num2: 0 

num: 0 

num2: 0 

num: 1
Copy the code

So we need a stack to hold the outer effect function so that the activeEffect pointer points to the outer effect. Revisit the source code:

function createReactiveEffect(fn, options) {...try{.../ / pressure stack
        effectStack.push(effect)
        activeEffect = effect
        
        // Execute the original function
        returnfn(... args) }finally {
        // Exit the stack
        effectStack.pop()

        // Restore the state before shouldTrack was started
        resetTracking()

        // point to the last effect on the stack
        activeEffect = effectStack[effectStack.length - 1]}}}...return effect
}
Copy the code

When effect calls logCount, it pushes logCount into the effectStack, and then inside of logCount, there’s another effect call logCount2 that pushes logCount2 into the effectStack. Num2 collects logCount2 (activeEffect) as a dependency, and effect executes the code in the finally area. Num = counter. Num = counter. Num = logCount; Because activeEffect points to logCount.

If counter. Num changes, logCount is executed.

Finally, there is an unexplained cleanUp function that removes the effect dependency:

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

In addition to collecting the currently activeEffect as a dependency, activeeffect.deps.push (dep) takes dep as an activeEffect dependency when the track function is executed. This way we can find the corresponding DEPs for effect at the cleanup and remove effect from those DEPs.

Why do you need cleanup? Consider the component’s rendering function as a side effect function, as in the following 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 is an example given by Huang Yi in Vue 3.0. His explanation is as follows:

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

ActiveEffect is a dependency on state. MSG when the template is rendered for the first time. We call it the render effect. Then we click the Switch View button, and the View switches to display random number. At this time, we click the Toggle Msg button, and since the state. Msg will send a notification, find the Render effect and execute it, and then trigger the re-rendering of the component.

However, this behavior is actually not expected, because when we click the Switch View button and the View switches to display random numbers, the component will also be re-rendered, but the View is not rendering state.msg, so the change to it should not affect the re-rendering of the component.

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

It’s a bit complicated, but read it a few times to see what cleanUp does.

So that’s what Reactive is all about.