In today’s article, I will take you through an in-depth analysis of Vue3’s reactive principle implementation and how Reactive is implemented in the reactive base API. For the Vue framework, its non-intrusive responsive system is one of the most unique features, so no matter any version of Vue, after familiar with its basic usage, responsive principle is the first part I want to understand, but also read the source code will be carefully studied. After all, it is not dangerous to know yourself and your enemy in a hundred battles. When you use Vue, mastering the principle of responsivity will definitely make your coding process more convenient.

Response formula principle of Vue2

Before introducing the reactive principle of Vue3, let’s review the reactive principle of Vue2.

When we pass a normal option to the Vue instance’s data option, Vue iterates through all of the Object’s properties and converts them into getters/setters using Object.defineProperty. Vue2 also changes the elements in the array by hijacking the prototype chain, observing new elements in the prototype chain, and issuing update notifications.

Here is an image from the Vue2 documentation that describes responsiveness. I will not repeat the descriptions in the documentation, but compare the images from the source of Vue2. There is an Observer module in the SRC /core path of Vue2’s source code, which is where reactive types are handled in Vue2. In this module the Observer is responsible for converting objects and arrays into responsive, purple, getters and setters for Data. When an option in data is accessed, the getter is triggered, and the Wather.js module in the Observer directory starts to collect dependencies. The dependencies we collect are instantiated objects of each Dep class. When the options in data are changed, the call of setter will be triggered, and in the process of setter, notify function of DEP will be triggered and update event will be issued, so as to realize the response monitoring of data.

Responsive variation of Vue3

After a brief review of the responsivity principle of Vue2, we will have a question: how is the responsivity principle of Vue3 different from that of Vue2?

The biggest difference between a responsive system in Vue3 is that the data model is a proxied JavaScript object. Whether we return a plain JavaScript object in the component’s data option or create a Reactive object using the Composition API, Vue3 wraps the object in a Proxy with get and SET handlers.

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

The basic syntax looks something like:

const p = new Proxy(target, handler)
Copy the code

What is the advantage of Proxy over Object.defineProperty? Let’s start with the drawbacks of Object.defineProperty.

From the Object perspective, since Object.defineProperty generates getters/setters for the specified key to track changes, a responsive system can’t do anything if the key doesn’t originally exist on the Object we define. Therefore, the addition or removal of an object’s property cannot be detected in Vue2. For this flaw, Vue2 provides vm.$set and a global VUe. set API that allows us to add responsive properties to objects.

From an array perspective, Vue2’s responsive system can’t listen for changes when we set an item directly using an index, or when we change the array length, using the two apis mentioned above.

Proxy objects can use handler traps to catch any changes in get and set. Proxy objects can also listen for changes in the array index and array length.

The way we rely on collecting and distributing updates is also different in Vue3, so here’s a quick overview: In Vue3, the processor function of track is used to collect dependencies and the processor function of trigger is used to distribute updates. The use of each dependency is wrapped in a side effect function, and the side effect function will be executed after the update is distributed, so that the value of the dependency is updated.

Implementation of reactive infrastructure

Since this is a source code analysis article, let’s analyze how responsive is implemented from the source point of view. Therefore, I will first analyze the reactive basic API — Reactive. I believe that by explaining the implementation of Reactive, you will have a deeper understanding of Proxy.

reactive

Without saying a word, directly look at the source code. The following functions of the Reactive API take an object as a parameter and, after being processed by the createReactiveObject function, return a proxy object.

export function reactive<T extends object> (target: T) :UnwrapNestedRefs<T>
export function reactive(target: object) {// If you attempt to observe a read-only proxy object, the read-only version is returnedif (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // Create a proxy object and return it
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
Copy the code

In line 3, you can see that the object is read-only by determining whether the IS_READONLY key in ReactiveFlags is present in the target. The ReactiveFlags enumeration will continue to appear in the source code, so it is worth introducing ReactiveFlags in advance:

export const enum ReactiveFlags {
  SKIP = '__v_skip'.// Whether to skip responsivity and return the original object
  IS_REACTIVE = '__v_isReactive'.// Mark a responsive object
  IS_READONLY = '__v_isReadonly'.// Marks a read-only object
  RAW = '__v_raw' // flag to get the original value
}
Copy the code

There are four enumerated values in the ReactiveFlags enumeration, and the meanings of the four enumerated values are in the comment. The use of ReactiveFlags is a good application of proxy objects to trap traps in handler. These keys do not exist in objects, and when accessed by GET, the return value is processed by the function of get trap. Now that we’ve introduced ReactiveFlags, let’s move on.

createReactiveObject

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
)
Copy the code

First look at the signature of the createReactiveObject function, which takes five arguments:

  • Target: The target object that you want to generate responsive primitives.
  • IsReadonly: indicates whether the generated proxy object is read-only.
  • BaseHandlers: Generates handler parameters for the proxy object. This handler is used when the target type is Array or Object.
  • CollectionHandlers: This handler is used when the target type is Map, Set, WeakMap, or WeakSet.
  • ProxyMap: Stores Map objects after proxy objects are generated.

The difference between baseHandlers and collectionHandlers is that these two parameters are determined based on the type of target and are passed to the Proxy constructor as a handler parameter.

Next we look at the logical part of the createReactiveObject:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // If the target is not an object, return the original value
  if(! isObject(target)) {return target
  }
  // If the target is already a proxy, return directly
  // Unless readonly is performed on a responsive object
  if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
  }
  // The target already has a proxy object
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // Only whitelisted types can be created
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
Copy the code

In the logical part of the function, you can see that the underlying data type is not converted to a proxy object, but rather returns the original value directly.

In addition, the generated proxy object will be cached into the incoming proxyMap. When the proxy object already exists, it will not be generated repeatedly and will directly return the existing object.

Vue3 will only generate proxy for Array, Object, Map, Set, WeakMap, WeakSet. Other objects will be marked as INVALID and return the original value.

When the target object passes type verification, a Proxy object Proxy is generated using new Proxy(). The handler parameter is also passed in relation to targetType and returns the generated Proxy object.

So looking back at the Reactive API, we might get a proxy object, or we might just get the raw value of the target object that was passed in.

The composition of Handlers

In the @vue/ Reactive library, there are baseHandlers and collectionHandlers modules, which generate trap traps in the Handlers of Proxy respectively.

For example, the baseHandlers argument in the API that generates reactive above passes a mutableHandlers object that looks like this:

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

We know from the variable names that there are five trap traps in the mutableHandlers. In baseHandlers, get and set are generated by factory functions to accommodate apis other than Reactive, such as Readonly, shallowReactive, shallowReadonly, and so on.

BaseHandlers are the data types that deal with arrays and objects, and that’s what we use most of the time with Vue3, so I’m going to focus on the Get and set traps in baseHandlers.

Get a trap

The previous section mentioned that get is generated by a factory function, so let’s look at the types of GET traps.

const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false.true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true.true)
Copy the code

There are four types of GET traps, each corresponding to a different responsive API. You can know the corresponding API name from the name. All gets are generated by createGetter. So let’s focus on the logic of createGetter.

So again, let’s start with the function signature.

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {}}Copy the code

CreateGetter has isReadonly and shallow parameters, which are required by apis that use get traps. The inside of the function returns a get function, which is passed to the Handlers as a higher-order function.

Now look at the logic of createGetter:

// If the key accessed by get is '__v_isReactive', the isReadonly parameter of createGetter is reversed
if (key === ReactiveFlags.IS_REACTIVE) {
  return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
  // If the key accessed by get is '__v_isReadonly', return the isReadonly parameter of createGetter
  return isReadonly
} else if (
  // If the key accessed by get is '__v_raw' and receiver is equal to the original identifier, the original value is returned
  key === ReactiveFlags.RAW &&
  receiver ===
    (isReadonly
      ? shallow
        ? shallowReadonlyMap
        : readonlyMap
      : shallow
        ? shallowReactiveMap
        : reactiveMap
    ).get(target)
) {
  return target
}
Copy the code

From this section of createGetter logic, the ReactiveFlags enumeration I specifically introduced comes in handy here. There are no such keys in the target object, but Vue3 does something special to them in GET, and when we access these special enumerated values on the object, we return meaningful results. IS_REACTIVE: reactiveFlags. IS_REACTIVE: reactiveFlags. IS_REACTIVE: reactiveFlags. IS_REACTIVE: reactiveFlags. IS_REACTIVE When an object is accessed to trigger the GET trap, it must already be a Proxy object, so anything that is not read-only is considered responsive.

Let’s look at the subsequent logic of GET.

If the proxy object is not read-only, target is an array, and the key is in the array’s special handling method, the special handling array function is called to execute the result and return it.

ArrayInstrumentations is an object that holds an array of specially treated methods, stored as key-value pairs.

We said earlier that Vue2 hijacks arrays in the form of prototype chains, and it does a similar thing here. Arrays will be covered in a later article. Here are the arrays that need special treatment.

  • Index sensitive array methods
    • Includes, indexOf, and lastIndexOf
  • Array methods that change their length need to be avoided by relying on length collection, as this can cause circular references
    • Push, pop, Shift, unshift, splice
// Check whether taeget is an array
const targetIsArray = isArray(target)
// If it is not a read-only object and the target object is an array, and the key is in the method that the array needs to be hijacked, call the modified array method directly
if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
}

// Get the default result of Reflect's get
const res = Reflect.get(target, key, receiver)

// If the key is a Symbol, and the key is a Symbol type key in the Symbol object
// Or keys are keys that do not need to be traced: __proto__,__v_isRef,__isVue
// Return the get result directly
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
  return res
}

// Not a read-only object, execute track to collect dependencies
if(! isReadonly) { track(target, TrackOpTypes.GET, key) }// If it is shallow, return the get result directly
if (shallow) {
  return res
}

if (isRef(res)) {
  // If it is ref, the unpacked value is returned - when target is array and key is int, unpacked is not required
  constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
}

if (isObject(res)) {
  // Convert the returned value to the proxy as well. Here we do isObject checks to avoid invalid value warnings.
  // Lazy access to read-only and star movie objects is also required here to avoid cyclic dependencies.
  return isReadonly ? readonly(res) : reactive(res)
}

// Return get if not object
return res
Copy the code

After processing the array, we execute reflect.get on target to get the get return value for the default behavior.

Then determine whether the current key is a Symbol or a key that does not need to be traced. If so, return the result of GET, res.

The following 👇 keys do not need to be relied upon to collect or return responsive results.

  • __proto__
  • _v_isRef
  • __isVue

It then determines whether the current proxy object is read-only and, if not, runs the tarck processor function mentioned above to collect the dependencies.

If it is shallow, it does not need to convert the internal properties into proxies and returns the RES directly.

If an RES is a Ref object, it will be unpacked automatically. Reactive unpacks refs automatically. Note that when target is an array type and key is an int, access to array elements using an index is not automatically unpacked.

If res is an object, it will be converted into a responsive Proxy object and returned. Combined with the cached generated Proxy object we analyzed earlier, we can see that the logic here does not generate the same RES repeatedly. It can also be understood that when we access a Reactive object, the key in the reactive object is automatically converted to a reactive object, and since generating a Reactive or readonly object here is a deferred action, It also helps that you don’t have to iterate through all the keys passed in by Reactive in the first place.

If the RES does not meet the above conditions, the RES result is returned directly. The underlying data type, for example, returns the result directly without special processing.

At this point, the logic of the GET trap is over.

Set a trap

Corresponding to createGetter, set also has a createSetter factory function, which also returns a set function by curryization.

Function signatures are much the same, so the next direct disk logic with you.

The set function is relatively short, so this time put up the commented code, first look at the code and then talk about the logic.

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) :boolean {
    let oldValue = (target as any)[key]
    if(! shallow) { value = toRaw(value) oldValue = toRaw(oldValue)// If it is not shallow, determine whether the old value is Ref, if so, update the value of the old value directly
      // Because ref has its own setter
      if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {
      // Shallow mode requires no special processing; the object is set as is
    }
		
    // Check whether there is a key in target
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // reflect. set Gets the return value of the default behavior
    const result = Reflect.set(target, key, value, receiver)
    // If the target is an attribute on the original object prototype chain, trigger will not be triggered to dispatch updates
    if (target === toRaw(receiver)) {
      // Use trigger to send out updates and call events according to hadKey
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

If the current proxy object is not a shallow comparison, the old value is determined to be a Ref. If the old value is not an array and is an object of type Ref, and the new value is not a Ref object, the value of the old value is directly modified.

Why update the value of an old value? If you have used the REF API, you will know that the value of each ref object is placed in value. The implementation of ref and Reactive is different. A REF is a class instance, and its value has its own set. So I’m not going to continue the set here. The REF part will be covered in more detail in a future article.

After processing values of type REF, a variable hadKey is declared to determine whether the key being set is an existing property of the object.

The next call to reflect. set gets the set return value result for the default behavior.

The process of issuing updates will then begin. Before issuing updates, you need to ensure that target is equal to the original receiver and that target cannot be a property on a prototype chain.

The trigger handler function is then used to send out updates, and if the hadKey does not exist, it is a new property, marked by the triggeroptypes.add enumeration. As you can see from the opening analysis, where the Proxy is stronger than Object.defineProperty, any new key will be detected, making the responsive system more powerful.

If the key is an existing attribute on the current target, the new and old values are compared. If the new and old values are different, the attribute is updated, and the update is signaled by triggeroptypes.set.

After the update is distributed, the result of set is returned, and the set ends.

conclusion

In today’s article, I first took you to review the responsive principle of Vue2, and then began to introduce the responsive principle of Vue3. By comparing the difference between Vue2 and Vue3 responsive system, the improvement of Vue3 responsive system is drawn. In particular, the most important adjustment was to replace Object.defineProperty with a Proxy Proxy Object.

In order to demonstrate the impact of attribute Proxy on reactive systems, this article focuses on the reactive base API: Reactive. We analyze the implementation of Reactive and the Handlers traps used by the Proxy object returned by the Reactive API. And the trap we most commonly used get and set source analysis, I believe that after reading this article, we have a new understanding of the use of proxy this ES2015 new feature.

This paper is only the first article to introduce Vue3 responsive system, so the process of collecting dependencies of Track and distributing updates of trigger is not expanded in detail. In the subsequent articles, the side effect function effect and the process of Track and trigger are planned to be explained in detail. If you want to understand the responsive system source code in detail, please point a concern to avoid getting lost.

If this article has helped you understand the reactive principle in Vue3 and the implementation of Reactive, I hope it will give you a thumbs up at ❤️. If you want to continue to follow the following articles, you can also follow my account, thank you again for reading so far.