Recently Vue officially opened 3.x source code, currently in the Pre Alpha stage, out of interest, I took time to Vue 3.x source code data responsive part to do a brief reading. In this paper, by analyzing the principle of Vue 3.x Reactive API, it is easier to understand the difference between Vue 3.x and Vue 2.x reactive principle.

Before the open source of Vue 3.x, the author has written about the principle of Vue Composition API responsive-wrapped object. The implementation of Vue 3.x Reactive API is similar to this. Students who are interested in Vue 3.

Before reading this article, if you don’t know enough about the following points, you should know the following points:

  • Proxy
  • WeakMap
  • WeakSet
  • Reflect
  • Vue Composition API

I have written about it before, and you can also combine it with related articles:

  • ES6 syntax you may have overlooked – reflection and proxy
  • Vue 3.0 update, Composition API
  • Vue 3.0 preview, experience Vue Function API
  • Vue Composition API responsively wraps object principles

Set up Vue 3.x running environment

Vue 3.x rollup: vue 3.x rollup: vue 3.x rollup: vue 3.x rollup: vue 3.x rollup Generate a file called vue.global.js for developers to reference. For easy debugging, we execute vue-next/scripts/dev.js, and enable rollup watch mode to debug, modify, and output the source code.

In project directory to create a new test. The HTML, references to build packages in the project directory/vue/dist/vue. Global. Js, perform in the project directory NPM run dev, wrote one of the most simple vue 3 x the demo, opened by the browser can run directly, Using this demo, we have built the basic Vue 3.x runtime environment, and now we can start debugging the source code.


      
<html>
<head>
    <title>vue-demo</title>
</head>
<body>
    <div id="app"></div>
    <script src="./packages/vue/dist/vue.global.js"></script>
    <script>
        const { createComponent, createApp, reactive, toRefs } = Vue;
        const component = createComponent({
            template: ` 
       
{{ count }}
`
, setup(props) { const data = reactive({ count: 0});const addHandler = (a)= > { data.count++; }; return{... toRefs(data), addHandler, }; }}); createApp().mount(component,document.querySelector('#app'));
</script> </body> </html> Copy the code

Reactive source code

Open the vue – next/packages/reactivity/SRC/reactive. Ts, first of all, you can find the reactive functions are as follows:

export function reactive(target: object) {
  // If it is a proxy for a readOnly object, the object is unobservable and the proxy for the readOnly object is returned
  if (readonlyToRaw.has(target)) {
    return target
  }
  // If the object is a readonly original object, then the object is also not observable. The proxy of the readOnly object is returned directly
  if (readonlyValues.has(target)) {
    return readonly(target)
  }

  Call createReactiveObject to create a Reactive object
  return createReactiveObject(
    target, // Target object
    rawToReactive, // Primitive object maps to responsive object WeakMap
    reactiveToRaw, // Reactive objects map to WeakMap of the original object
    mutableHandlers, // Proxy handlers for responsive data, typically Object and Array
    mutableCollectionHandlers // The proxy handler of the responsive Set is generally Set, Map, WeakMap, WeakSet)}Copy the code

Reactive is a readonly object. If the target object is a readonly object or the proxy object returned by calling ue. Readonly, it is not a readonly object. The readOnly responsive proxy object is returned directly. CreateReactiveObject is then called to create the reactive object.

The five parameters passed by createReactiveObject are: Target Object, original Object mapping responsive Object WeakMap, responsive Object mapping original Object WeakMap, responsive data proxy handler, generally Object and Array, responsive Set proxy handler, generally Set, Map, WeakMap, WeakSet. We can turn to the vue – next/packages/reactivity/SRC/reactive. Ts top, can see defines the following constants:

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()

// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()

const collectionTypes = new Set<Function> ([Set.Map.WeakMap.WeakSet])
Copy the code

It can be seen that the following four Weakmaps are pre-stored in Reactive: RawToReactive, reactiveToRaw, rawToReadonly, and readonlyToRaw map raw objects to reactive objects and readOnly proxy objects to raw objects respectively. In addition, readonlyValues and nonReactiveValues are defined, which are the collections of readOnly proxy objects and those that call ue. MarkNonReactive and mark them as not corresponding objects respectively. CollectionTypes is the Set of Set, Map, WeakMap and WeakSet

The reason why WeakMap is used for mutual mapping is that WeakMap’s key is weakly referenced. In addition, compared with Map, the algorithm complexity of assignment and search operation of WeakMap is lower than that of Map. For specific reasons, please refer to relevant documents.

Let’s look at createReactiveObject:

function createReactiveObject(target: unknown, toProxy: WeakMap
       
        , toRaw: WeakMap
        
         , baseHandlers: ProxyHandler
         
          , collectionHandlers: ProxyHandler
          
         
        ,>
       ,>) {
  // If it is not an object, return it directly, and the development environment will warn you
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
  ToProxy is rawToReactive. WeakMap is used to map reactive proxies
  let observed = toProxy.get(target)
  if(observed ! = =void 0) {
    return observed
  }
  // The target object is already a responsive Proxy, so it directly returns a responsive Proxy. ToRaw is a reactiveToRaw WeakMap, which is used to map the original object
  if (toRaw.has(target)) {
    return target
  }
  // The target object is unobservable and returns the target object directly
  if(! canObserve(target)) {return target
  }
  // Here is the core logic for creating a responsive proxy
  // The responsive Object handler of Set, Map, WeakMap, WeakSet is different from the responsive Object handler of Object and Array
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  / / create the Proxy
  observed = new Proxy(target, handlers)
  // Update the mapping between rawToReactive and reactiveToRaw
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  Reactive (targetMap) reactive (targetMap) reactive (targetMap) reactive
  if(! targetMap.has(target)) { targetMap.set(target,new Map()}return observed
}
Copy the code

Looking at the code above, we know that createReactiveObject is used to create reactive proxy objects:

  • In the first place to judgetargetIf it is not an object, return it directly. The development environment will give a warning
  • Then determine whether the target object is already observable. If so, return the created reactive Proxy directly.toProxyisrawToReactivethisWeakMapIs used to map reactive proxies
  • Then determine whether the target object is already a reactive Proxy. If so, return a reactive Proxy directly.toRawisreactiveToRawthisWeakMapIs used to map the original object
  • Then create a responsive proxy forSet,Map,WeakMap,WeakSetThe responder object handler andObjectandArrayThe responder object handler is different from the responder object handler
  • The last updaterawToReactiveandreactiveToRawmapping

Reactive proxy traps

Object and Array proxies

Below the center of gravity to the analysis of mutableCollectionHandlers and mutableHandlers, firstly analysis the vue – next/packages/reactivity/SRC/baseHandlers ts, This handler is used to create responsive proxies of type Object and type Array:

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}
Copy the code

As we know, the most important are proxy get traps and set traps. Let’s start with get traps:

function createGetter(isReadonly: boolean) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // Reflect gets the original get behavior
    const res = Reflect.get(target, key, receiver)
    // If it is a built-in method, no additional proxy is required
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    // If it is a ref object, proxy to ref.value
    if (isRef(res)) {
      return res.value
    }
    // track is used to collect dependencies
    track(target, OperationTypes.GET, key)
    // If it is a nested object, it needs to be handled separately
    // If it is a primitive type, return the value directly
    return isObject(res)
      // createGetter is used to create a responsive object. IsReadonly is passed in as false
      // For nested objects, call Reactive recursively to get the result
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}
Copy the code
  • Get traps pass firstReflect.getGet the original GET behavior
  • Then determine that if it is a built-in method, no additional proxy is required
  • Then determine if it is a ref object, proxy to ref. Value
  • Then throughtrackTo collect dependencies
  • I got it at lastresIf the result is an object type, call Reactive (RES) again to get the result to avoid cyclic references

Here’s the set trap:

function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
  // Get the original oldValue
  value = toRaw(value)
  const oldValue = (target as any)[key]
  // If the original value is a ref object and the new assignment is not a ref object, modify the value attribute of the ref wrapper object directly
  if(isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
  }
  // Whether the original object has the newly assigned key
  const hadKey = hasOwn(target, key)
  // Reflect gets the original set behavior
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  // Manipulate the data in the prototype chain without doing anything to trigger the listener
  if (target === toRaw(receiver)) {
    /* istanbul ignore else */
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      // Without this key, the attribute is added
      // The original attribute is assigned otherwise
      // trigger is used to notify DEPS of updates to objects that depend on this state
      if(! hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) }else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if(! hadKey) { trigger(target, OperationTypes.ADD, key) }else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }
  return result
}
Copy the code
  • The set trap gets the original value firstoldValue
  • And then to determine if the original value is zerorefObject, new assignment is notrefObject, directly modifiedrefObject wrappervalueattribute
  • Then throughReflectGet the original set behavior, if the original object has the newly assigned key, no key, then add the property, otherwise assign the original property
  • Perform the corresponding modification and add attribute operation through the calltriggernoticedepsUpdate, notifying objects that depend on this state of updates

Set, Map, WeakMap, WeakSet agent

MutableHandlers is analyzed, the following to analyze mutableCollectionHandlers, open the vue – next/packages/reactivity/SRC/collectionHandlers ts, WeakMap, WeakMap, WeakSet responsive Proxy this handler is used to create Set, Map, WeakMap, WeakSet responsive Proxy:

// The method call to listen on
const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size(this: IterableCollections) {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false)}// ...


function createInstrumentationGetter(
  instrumentations: Record<string, Function>
) {
  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) =>
    // If the 'get', 'has',' add ', 'set', 'delete', 'clear', 'forEach' method calls, or obtain 'size', then call the relevant mutableInstrumentations method instead
    Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
}

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(mutableInstrumentations)
}
Copy the code

Look at the code, we see only a get mutableCollectionHandlers trap, is this why? Because of the limitations of the internal mechanism of Set, Map, WeakMap and WeakSet, the operation of modifying and deleting properties can be completed by means of Set, Add and delete, etc., which cannot be monitored by Proxy setting Set trap, similar to the implementation of variation method of Vue 2.x array. Responsivity is achieved by listening for get, HAS, add, set, delete, clear, forEach method calls in the GET trap and intercepting the method calls.

As for why Set, Map, WeakMap and WeakSet cannot be responsive, the author found the answer in why-is-set-set-with-proxy.

So we understand that the limitation of Proxy on Set, Map, WeakMap and WeakSet is similar to the variation method of Vue 2.x. Intercept method calls of GET, HAS, add, set, delete, clear and forEach to monitor the modification of set, Map, WeakMap and WeakSet data types. It is much easier to look at methods like Get, HAS, add, set, delete, clear, forEach, etc. These methods are similar to get, HAS, set and other trap handlers for object types, and I won’t go into too much detail here.

summary

In this paper, the author continues to pay attention to the dynamics of Vue 3.x. First, the author describes how to build a simple running and debugging environment for Vue 3.x code, and then analyzes the core principle of Vue 3.x responsiveness. Compared with Vue 2.x, Vue 3.x fully embraces the Proxy API for the reactive aspect and implements the reactive through the default behavior of the Proxy initial object. Reactive makes use of weak-reference properties and fast indexing of WeakMap, and uses WeakMap to preserve responsive proxy and original object. Readonly proxy and original object map each other. Finally, the author analyzes the relevant trap method of responsive Proxy, and it can be known that for object and array types, the original object responsiveness is realized through the relevant trap method of responsive Proxy, while for Set, Map, WeakMap, WeakSet types, due to the limitation of Proxy, Vue 3.x implements the reactive principle by hijacking get, HAS, add, set, Delete, clear, forEach and other method calls.