Vue 3 responsive core principle analysis

Reactive Reactivity is one of the modules that has changed more in Vue 3 than in Vue 2, and is also the module that has seen the most performance improvements. The core change is to use ES 6 Proxy API to replace Object. DefineProperty method in Vue2 to implement responsivity. What is the Proxy API and how to implement responsivity in Vue 3 will be revealed below.

Proxy API

The Proxy object corresponding to the Proxy API is a native object introduced in ES6 that defines the custom behavior of basic operations (such as property lookup, assignment, enumeration, function call, and so on). Literally, a Proxy object is a Proxy for a target object, through which any operation on the target object (instantiation, adding/deleting/modifying properties, and so on) must pass. So we can block and filter or modify all operations from the outside world. These proxy-based features are often used to:

  • Create a “reactive” object, such as the Reactive method in Vue3.0.
  • Create an isolated JavaScript “sandbox.”

The basic syntax for Proxy is shown in the following code:

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

The target parameter represents the target object to be wrapped with Proxy (which can be any type of object, including a native array, a function, or even another Proxy), and the handler parameter represents the object with functions as properties, which define the behavior of Proxy P when performing various operations. The common usage method is as follows:

let foo = { a: 1, b: 2 } let handler = { get:(obj,key)=>{ console.log('get') return key in obj ? Obj [key] : undefined}} let p = new Proxy(foo,handler) console.log(p.a) // Print 1Copy the code

In the code above, p is foo’s Proxy object, and all operations on p are synchronized to Foo. Proxy also provides another method to generate Proxy objects, proxy.revocable (), as shown in the following code:

const { proxy,revoke } = Proxy.revocable(target, handler)
Copy the code

The return value of this method is an object of the structure {“proxy”: proxy, “REVOKE “: Revoke}, where: Revoke Is a revoke method that can be used to revoke the created proxy object without adding any arguments. The following code looks like this:

let foo = { a: 1, b: 2 } let handler = { get:(obj,key)=>{ console.log('get') return key in obj ? obj[key] : Use undefined}} let {proxy,revoke} = proxy.revocable (foo,handler) console.log(proxy.a) Console. log(proxy.a) // Error: Uncaught TypeError: Cannot perform 'get' on a proxy that has been revokedCopy the code

Note that once a proxy object is destroyed, it becomes almost completely uncallable, and any proxiable operation performed on it throws TypeError exceptions. In the above code, we only use the handler for the get operation, that is, when we try to get a property of the object. In addition, the Proxy has nearly 14 handlers, which can also be called hooks. They are:

Handler.getprototypeof () : This operation is triggered when the prototype of a proxy Object is read, such as when Object.getProtoTypeof (proxy) is executed. Handler.setprototypeof () : This action is triggered when the prototype of a proxy Object is set, such as when Object.setPrototypeof (proxy, null) is executed. Handler.isextensible () : this operation is triggered when determining whether a proxy Object isExtensible, such as when object.isextensible (proxy) is executed. Handler. PreventExtensions () : when he makes a proxy Object cannot extend trigger the operation, such as the execution Object. PreventExtensions (proxy). Handler. GetOwnPropertyDescriptor () : in acquiring a proxy Object a property description is triggered when the operation, such as the execution Object. GetOwnPropertyDescriptor (proxy, "foo"). Handler.defineproperty () : This action is triggered when a property description is defined for an attribute of a proxy Object, such as when Object.defineProperty(proxy, "foo", {}) is executed. Handler.has () : This action is triggered when determining whether the proxy object has a property, such as when executing "foo" in proxy. Handler.get () : Triggered when reading a property of a proxy object, such as when proxy.foo is executed. Handler.set () : Triggered when assigning a value to an attribute of a proxy object, such as proxy.foo = 1. Handler.deleteproperty () : This operation is triggered when an attribute of the proxy object is deleted, using the DELETE operator, such as when delete proxy.foo is executed. Handler. OwnKeys () : when the execution Object. GetOwnPropertyNames (proxy) and Object. GetOwnPropertySymbols triggered (proxy). Handler.apply () : Triggered when the apply() method is called when the proxy object is a function function, such as proxy.apply(). Handler.construct () : emitted when the proxy object is a function and instantiated with the new keyword, such as new proxy().Copy the code

Together with these handlers, we can implement restrictions on objects, such as disallowing the deletion or modification of an object property, as shown in the following code:

let foo = { a:1, b:2 } let handler = { set:(obj,key,value,receiver)=>{ console.log('set') if (key == 'a') throw new Error('can not change  property:'+key) obj[key] = value return true }, deleteProperty:(obj,key)=>{ console.log('delete') if (key == 'a') throw new Error('can not delete property:'+key) delete Obj [key] return true}} let p = new Proxy(foo,handler) Uncaught Error // Try to delete attribute a delete p.a // Error message: Uncaught Error is reportedCopy the code

In the above code, the set method has a receiver parameter, which is usually the Proxy itself, i.e. P. In the scenario where there is a code executing obj. Name =”jen”, OBj is not a Proxy and does not contain the name attribute, but it has a Proxy on its prototype chain, then, The set method in the proxy handler is called, and obj is passed in as receiver. Verify the property changes as shown in the following code:

let foo = { a:1, b:2 } let handler = { set:(obj,key,value)=>{ console.log('set') if (typeof(value) ! == 'number') throw new Error('can not change property:'+key) obj[key] = value return true } } let p = new Proxy(foo,handler) p.a = 'hello' // Uncaught Error message is reportedCopy the code

Proxy can also listen for array changes, as shown in the following code:

Let arr = [1] let handler = {set(obj, key,value)=>{console.log('set') // print set return reflect.set (obj,key,value); }} let p = new Proxy(arr,handler) p.ush (2Copy the code

Reflect.set() is used to modify the value of an array, returning a Boolean type, which is also compatible with the case of the method on the array prototype, equivalent to obj[key] = value.

Proxy and reactive objects

In Vue 3, the reactive object method is used as follows:

import {ref,reactive} from 'vue'.setup(){
  const name = ref('test')
  const state = reactive({
    list: []})return {name,state}
}
...
Copy the code

In Vue 3, the Composition API often uses the method ref/ Reactive to create responsive objects, which is internally implemented by Proxy API. In particular, the set method of handler can realize the logic related to two-way data binding. This is a big change from Object.defineProperty() in Vue 2, with major improvements as follows:

  • Object.defineProperty()It can only monitor the modification or change of existing attributes, and cannot detect the addition or deletion of object attributes ($set() is used in Vue 2), while Proxy can be easily implemented.
  • Object.defineProperty()The inability to listen for reactive data types is for array changes (mainly array length changes, which Vue 2 solved by overwriting array methods and adding hooks), whereas proxies can be easily implemented.

Because of the nature of Proxy, it is easy to implement the above two capabilities using object.defineProperty () in a very complicated way without any configuration using Proxy’s native features.

The operation principle of ref() method

In the Vue 3 source code, all the reactive code is under vue-next/package/reactivity, where reactivity/ SRC /index.ts exposes all the available methods. Let’s take a look at how Vue 3 leverages Proxy using the common ref() method as an example. The main logic of the ref() method is in reactivity/ SRC /ref.ts, which looks like this:

.// The entry method
export function ref(value? : unknown{
  return createRef(value, false)}function createRef(rawValue: unknown, shallow: boolean{
  // rawValue indicates the original object and shallow indicates whether it is recursive
  // If it is already a ref object, return it directly
  if (isRef(rawValue)) {
    return rawValue
  }
  Create a new RefImpl object
  return new RefImpl(rawValue, shallow)
}
...
Copy the code

The second argument received by the createRef method is shallow, indicating whether it is a recursive listening response, which corresponds to the other responsive method, shallowRef(). In RefImpl constructor, there is a value attribute, this property is by toReactive () method returns, toReactive () method on the reactivity/SRC/reactive. Ts file, as shown in the following code:

class RefImpl<T{...constructor(value: T, public readonly _shallow: boolean) {
    this._rawValue = _shallow ? value : toRaw(value)
    // Call toReactive if it is non-recursive
    this._value = _shallow ? value : toReactive(value)
  }
  ...
}
Copy the code

In reactive. Ts, you start to actually create a reactive object, as shown in the code below:

export function reactive(target: object{
  // If it is readonly, it is returned directly without adding responsiveness
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,// The original object
    false./ / is readonly
    mutableHandlers,// Proxy baseHandlers
    mutableCollectionHandlers,// The proxy handler object collectionHandlers
    reactiveMap// Proxy object mapping)}Copy the code

CreateReactiveObject () passes two handlers, baseHandlers and collectionHandlers. If the target type is Map, Set, WeakMap, WeakSet uses collectionHandlers, whose type is Object, and Array uses baseHandlers. If it is a basic Object, it does not create a Proxy Object. ReactiveMap stores the mapping of all responsive objects. Used to avoid duplicate create responses for the same object. Let’s look at the implementation of the createReactiveObject() method, as shown below:

function createReactiveObject(.{
  // If target does not satisfy typeof val === 'object', target is returned directly
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
  // If target is already a proxy object or is read-only, return it directly
  // exception: calling readonly() on a reactive object
  if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
  }
  // If the target has already been created as a Proxy object, this object is returned directly
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // Only targets that match the type can be created responsive
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // Call the Proxy API to create responsiveness
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // Marks that the object has been created reactive
  proxyMap.set(target, proxy)
  return proxy
}
Copy the code

You can see that in the createReactiveObject() method, the main things are done:

  • Prevent read-only and duplicate creation of reactive.
  • Select different handlers for different target types.
  • Create a Proxy object.

Will call new Proxy objects to create response type, baseHandlers, for example, we look at how the handler implementation, the reactivity/SRC/baseHandlers ts can see this part of the code, mainly implements these handler, The following code looks like this:

const get = /*#__PURE__*/ createGetter()
...
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
Copy the code

Take handler.get as an example to see what happens internally. When we try to read an object’s properties, we enter the get method, whose core code looks like this:

function createGetter(isReadonly = false, shallow = false{
  return function get(target: Target, key: string | symbol, receiver: object{
    if (key === ReactiveFlags.IS_REACTIVE) { // If the key of the accessed object is __v_isReactive, return the constant
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {// If the key of the access object is __v_isReadonly, the constant is returned
      return isReadonly
    } else if (// Return target directly if the access object's key is __v_raw, or the original object is read-only, etc
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }
    // If target is an array type
    const targetIsArray = isArray(target)
    // If the key is a native method of the array, return the result of the call
    if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
    }
    / / evaluated
    const res = Reflect.get(target, key, receiver)
    // Check whether the accessed key is Symbol or does not require responsive keys such as __proto__,__v_isRef,__isVue
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
    // Collect the response for later effect methods to detect
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }// If the binding is non-recursive, return the result directly
    if (shallow) {
      return res
    }

    // If the result is already responsive, check the type first and return
    if (isRef(res)) {
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }

    // If the result of the current key is also an object, call reactive again to perform the reactive binding logic on the changed object
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    // Return the result
    return res
  }
}
Copy the code

The above code is one of the core code of Vue 3 responsiveness, its logic is relatively complex, readers can understand according to the comments, summarized, this code mainly does the following things:

  • forhandler.getMethod will eventually return the result of the current object’s key, i.eobj[key], so the code will eventually return the result.
  • Returns the corresponding result for non-responsive keys and read-only keys.
  • For array targets and key values that are prototypical methods such as includes, push, pop, etc., Reflect. Get.
  • Add listener track in Effect for responsive listener service.
  • When the result of the current key is an object, to ensure that the set method can be triggered, the object needs to be recursively bound to reactive().

The handler. Get method returns a value, so let’s look at what handler. Set does.

function createSetter(shallow = false{
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,// The new value to be set
    receiver: object
  ) :boolean {
    // Cache old values
    let oldValue = (target as any)[key]
    if(! shallow) {// Convert the old and new values to the original object
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // If the old value is already a RefImpl object and the new value is not a RefImpl object
      / / such as var v = Vue. Reactive ({a: 1, b: Vue. Ref ({3} c:)}) set of scenarios
if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = value// Assign the new value directly to the old responsive object
        return true}}// Check whether a new key is added or the value of the key is updated
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Set the result of the set and add the listener effect logic
    const result = Reflect.set(target, key, value, receiver)
    // Determine whether target has been moved, including adding or removing items from the prototype
    if (target === toRaw(receiver)) {
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value)// Add key trigger listener
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)// Update key trigger listener}}// Return the set result true/false
    return result
  }
}
Copy the code

The core function of handler.set method is to set the corresponding value of key, namely obj[key] = value, and to judge and process the old and new values logically. Finally, trigger trigger track logic is added to trigger effect. If readers find the above source code difficult to understand, I remove some boundaries and compatibility judgment, the whole process is combed and simplified, can refer to the following easy-to-understand code:

let foo = {a: {c:3.d: {e:4}},b:2}
const isObject = (val) = >{
    returnval ! = =null && typeof val === 'object'
}
const createProxy = (target) = >{
    let p = new Proxy(target,{
        get:(obj,key) = >{
            let res = obj[key] ? obj[key] : undefined

            // Add a listener
            track(target)
            // Determine the type to avoid endless loops
            if (isObject(res)) {
                return createProxy(res)// loop recursive calls
            } else {
                return res
            }
        },
        set(obj, key, value) = > {
          console.log('set')
          
          obj[key] = value;
          // Trigger the listener
          trigger(target)
          return true}})return p
}

let result = createProxy(foo)

result.a.d.e = 6 // Print out set
Copy the code

When attempting to modify the attributes of a multi-layer nested object, the get method of the object at the upper level of the attribute will be triggered. Using this method, the object at each level can be added with Proxy Proxy, thus realizing the attribute modification problem of multi-layer nested object. On this basis, track and trigger logic will be added simultaneously. This completes the basic reactive process. We will explain track and trigger processes in detail in later chapters with bidirectional binding.