Vue3 source code parsing 06 – Responsive baseHandler

preface

I have studied the source code implementation of Reactive and REF. Through the source code, we know that reactive is implemented through Proxy data hijacking. Let’s take a look at the definition of Proxy:

Proxy object The user creates a Proxy for an object, thus intercepting and customizing basic operations (such as property lookup, assignment, enumeration, function call, etc.). Proxy receives two parameters target and handler. Target is the target object that we want to wrap. Handler Is an object that typically has functions as attributes. The functions in each attribute define the behavior of the agent to generate the object when performing various operations.

Reactive processor object Handler (reactive) ¶

Let’s review our target data type

const enum TargetType {
  INVALID = 0.// Other types
  COMMON = 1.// Basic data types of Array and Object
  COLLECTION = 2 //Set, Map, WeakMap, WeakSet Set data type
}
Copy the code

PS: Because it is quite long, I will write it in two parts.

BaseHandlers source code implementation

BaseHandlers are for basic data types. There are four main handlers :mutableHandlers, readonlyHandlers, shallowReactiveHandlers, and shallowReadonlyHandlers. The four types are normal, readonly, shallow, and so on for the target data. All three objects except mutableHandlers can be considered mutableHandlers variants. So let’s look at the source implementation of mutableHandlers:

// Create responsive proxies of type Object and type Array
export const mutableHandlers: ProxyHandler<object> = {
  get, //creatorGetter()
  set, //createSetter()
  deleteProperty,
  has,
  ownKeys
}
Copy the code

We know that the mutableHandlers object is the object that handles the handlers, so the functions of these methods in the mutableHandlers object are described as follows:

  • Handler.get: The trap for the property read operation
  • Handler. set: The catcher for the property setting operation
  • Handler. DeleteProperty: delete operator’s trap
  • Handler. OwnKeys: for interception Object. GetOwnPropertyNames, Object, getOwnPropertySymbols, Object. The keys (), Reflect. OwnKeys (), and other operations

Here are five methods. Let’s go straight to the source code.

createGetter

Take a look at the get source code:

function createGetter(isReadonly = false, shallow = false) {
  //target: indicates the data to be processed. Key: indicates the data key
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
  // Reflect gets the original get behavior
    const res = Reflect.get(target, key, receiver)

    // If it is a built-in method, no additional processing is required
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }

    // If not readonly, track is used for dependency collection
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
      return res
    }
    // if it is a ref object,
    if (isRef(res)) {
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }
    // If it is a nested object, handle it separately
    if (isObject(res)) {
      // Create a responsive object with isReadonly as false
      // For nested objects, call Reactive recursively to get the result
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}
Copy the code

The get function is simple: it just returns the corresponding data based on the passed key.

Let’s look at the createGetter code:

  • First, reflect. get gets the original get method
  • Determine that if it is a built-in method, no additional processing is required
  • Next, if it is not readonly, it does dependency collection
  • Check if it is a REF object. Here’s an additional judgment: for non-shallow modes, for targets that are not arrays, the value of ref.value is taken directly, not ref
  • Finally, if it is a nested object, additional processing is performed depending on the isReadonly parameter.

createSetter

Next, take a look at the createSetter source code. Before looking at createSetter, let’s review the types of trigger

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}
Copy the code

There are four types: modify, Add, delete, and clear (which occurs in collection type data)

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) :boolean {
    // Get the original oldValue
    const oldValue = (target as any)[key]
    // If target is an object and oldValue is ref and value is not ref, set value directly to oldvalue.value
    if(! shallow) { value = toRaw(value)if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {
      // In shallow mode, objects are set as they are, regardless of whether they are reactive or not
    }
    // Whether the original object has the newly assigned key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Reflect gets the original set method
    const result = Reflect.set(target, key, value, receiver)
    // Manipulate the data of the prototype chain without doing anything to trigger the listener
    if (target === toRaw(receiver)) {
      // Without the key, add the attribute
      // Otherwise assign the original attribute
      //trigger is used to notify DEPS of updates to objects that depend on this state
      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

Here’s a quick look at the createSetter source code:

  • First, in the non-shallow case, if the original object is not an array, the old value is ref and the new value is not ref. Value is directly modified
  • Operate on the data of the prototype chain without doing anything to trigger the behavior of the listener function. These operations mainly include:
    • In the case that the original data does not contain the current key that needs to be modified, dependent updates are triggered and the trigger type is set to Add
    • In the case that the raw data contains the current key that needs to be changed, the trigger depends on update and the trigger type is set to set
  • Finally, Reflect returns the original set action

The functions of PS:trigger have been introduced in previous articles, so I don’t want to repeat them here

deleteProperty

The deleteProperty here is a hijacking of the delete operator. Let’s take a quick look at his implementation

function deleteProperty(target: object, key: string | symbol) :boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
Copy the code

This code is very simple: it displays a key that determines whether the current data needs to be deleted, and then retrieves the corresponding value. If both exist, the trigger method is called to trigger the update dependent operation. Finally returns the original action of the Reflect deleteProperty.

PS: Because the source code of HAS and ownKeys is relatively simple, there will be no more details here.

readonlyHandlers

We all know that mutableHandlers correspond to normal responsive data, so readonlyHandlers correspond to read-only readonly data. Let’s take a look at his simple implementation:

//packages/reactivity/src/baseHandlers.ts
// Create a response for data of type readonly
export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true}}Copy the code

Here the source code is very simple, set and deleteProperty methods in development mode will give the corresponding prompt, tell the user the current data is not operable.

For the get method, we can see from the above code that the track operation will not take place:

function createGetter(isReadonly = false, shallow = false) {
  // If it is readonly, no track operation will be performed
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) } }Copy the code

The source code for shallowReactiveHandlers and shallowReadonlyHandlers is pretty much the same, so I won’t go into it here.

summary

To summarize, baseHandlers are:

  • Data hijacking by Proxy returns new responsive data
  • Reflect reflects our operations on reactive data back to the raw data
  • Dependency collection and listening methods are triggered during reads and writes