preface

Reactive: shallowReactive: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly: shallowReadonly

Provide data to intercept processing method: vue – next3.2 / packages/reactivity/SRC/baseHandlers ts

Give the array collection intercept processing method: vue – next3.2 / packages/reactivity/SRC/collectionHandlers ts

This article is a line by line analysis of all my understanding and harvest of these two files, and I share them with you to record my learning process

Tool function

Before we do that, let’s look at a couple of utility functions,

  • IsRef checks whether the type isRef
export function isRef<T> (r: Ref<T> | unknown) :r is Ref<T>
export function isRef(r: any) :r is Ref {
  return Boolean(r && r.__v_isRef === true)}Copy the code
  • IsIntegerKey Specifies whether the key is a number
export const isIntegerKey = (key: unknown) = >isString(key) && key ! = ='NaN' &&
  key[0]! = =The '-' &&
  ' ' + parseInt(key, 10) === key
Copy the code
  • IsObject Indicates whether it is an object
export const isObject = (val: unknown): val is Record<any.any> => val ! = =null && typeof val === 'object'
Copy the code
  • HasOwn determines whether the key exists in the object
// Get the hasOwnPrototype from the prototype to currize
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
  val: object.key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
Copy the code
// Convert to a responsive proxy object
consttoReactive = <T extends unknown>(value: T): T => isObject(value) ? Reactive (value) : value const toReadonly = <T extends Unknown >(value: T): T => isObject(value)? Readonly (value as Record<any, any>) : value // Const toShallow = <T extends Unknown >(value: T): Const getProto = <T extends CollectionTypes>(v: T): any => reflect.getProtoTypeof (v)Copy the code

baseHandler

Data read interception method

Take two parameters

IsReadonly Read-only proxy object?

Shallow shallow proxy?

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // reactive validates the readonly
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      // The proxy object already exists (there are four collections storing different proxy objects)
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    ArrayInstrumentations operate if the array method is the name of the array native method that has been intercepted
    if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // Map to the original object
    const res = Reflect.get(target, key, receiver)

    // Symbol cannot be a special attribute (__proto__,__v_isRef,__isVue)

    // If the value is an array, or a ref object with a numeric key, it cannot be expanded and returned directly
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }
    
    // Do not do dependency collection if it is read-only
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }// Return the value if it is not an object
    // Otherwise responsive data is returned according to isReadonly
    /* Here is lazy loading processing to get the target's internal data is not reactive, here is reactive processing of the object's internal data and then returns the proxy object */
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

// Finally use this method to generate some GET interceptor methods
const get = /*#__PURE__*/ createGetter() // Intercepting proxy get method for variable data
const shallowGet = /*#__PURE__*/ createGetter(false.true) // The interception proxy get method for shallow variable data
const readonlyGet = /*#__PURE__*/ createGetter(true) // Intercepting proxy methods for immutable data
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true.true) // Intercepting proxy methods for shallow immutable data
Copy the code

Object. Defineproperty is used as a shuttle in vue2 to process data from start to end. However, if a new data is referenced halfway, it cannot be used as a Proxy for the new data. However, data broker processing can be performed on the returned data when the data is retrieved. See data modification interception below

Data modification interception method

The create method takes one parameter: shallow: shallow?

The return interception method takes four arguments:

Target: target data key: key value value: new attribute value Receiver: Target proxy object

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) :boolean {
    // Get the old attribute value
    let oldValue = (target as any)[key]
    // Only the old values that are not shallow are of type ref and the new values are not directly modified on the old values
    if(! shallow) { value = toRaw(value) oldValue = toRaw(oldValue)if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {
      // In shallow mode, the default Settings are used regardless of whether the object is a proxy
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    // Check whether the key exists in the object.
    const hadKey =
      // Is an array and the key is an integer
      isArray(target) && isIntegerKey(key)
        // The index cannot be greater than the length of the array
        ? Number(key) < target.length
        // The key value exists in the existing object.
        : hasOwn(target, key)
        // Map to the original object
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    // Receiver must be a proxy object for target to trigger
    // Receiver: the object that is called initially. This is usually the proxy itself, but the handler's set method may also be called indirectly on the prototype chain or in other ways (so not necessarily the proxy itself)
    if (target === toRaw(receiver)) {
      // The old value is modified but the new value is not added
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

// Use this method to generate the following two set methods
const set = /*#__PURE__*/ createSetter() // Set method for intercepting mutable data
const shallowSet = /*#__PURE__*/ createSetter(true) // Set method for intercepting shallow variable data
// Special handling is required for read-only cases
Copy the code

There are a few points above that need to be explained in detail:

  • ! isArray(target) && isRef(oldValue) && ! isRef(value)

    IsRef (oldValue) &&! IsRef (value) means that the old value is the ref type and the new value is not, and this code is a good example of that

    const {ref, reactive, createApp} = vue
    setup() {
         let count = ref(0)
         const state = reactive({
           count
         })
    
         setTimeout(() = > {
           state.count = 30
         }, 1000)
    
         return {
           state
         }
       }
    
    Copy the code
  • About the meaning of the point target === toRaw(receiver)

    Before, I just thought that receiver is the Proxy object of target, which should be absolutely equal to target after converting the original object. After reading some articles, I finally found the link address of detailed answer: Juejin. Cn/post / 684490…

  • If you use the array native method to change the array, it will inevitably trigger two sets or even infinite calls, so VUe3.2 hijacks the five methods of changing the array itself

function createArrayInstrumentations() {
  const instrumentations: Record<string.Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values
  // 3 methods to determine if a value exists in an array; (['includes'.'indexOf'.'lastIndexOf'] as const).forEach(key= > {
    instrumentations[key] = function(this: unknown[], ... args: unknown[]) {
      const arr = toRaw(this) as any
      // Collect dependencies
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + ' ')}// we run the method using the original args first (which may be reactive)
      // Run the method for the first time with the parameters passed in
      constres = arr[key](... args)if (res === -1 || res === false) {
        // Convert the proxy object to raw data and run it again and return
        // if that didn't work, run it again using raw values.
        returnarr[key](... args.map(toRaw)) }else {
        return res
      }
    }
  }) 
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137)
  // 5 methods that modify the array itself; (['push'.'pop'.'shift'.'unshift'.'splice'] as const).forEach(key= > {
    instrumentations[key] = function(this: unknown[], ... args: unknown[]) {
      // In version 3.0, VUE collects and fires dependent methods like array push, which can cause infinite loop calls
      /** * watachEffect(() => { * arr.push(1) * }) * * watchEffect(() => { * arr.push(2) * }) */
      pauseTracking()
      // Execute the method on the array native to return the result
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetTracking()
      return res
    }
  })
  return instrumentations
}
Copy the code

That is, when a native method is called to alter an array, instead of collecting dependencies and triggering updates, we agree to call a unique mount update function for each component

Other interception methods

// Intercept and delete data
function deleteProperty(target: object, key: string | symbol) :boolean {
// Check whether the key exists and delete the operation
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  // The update will be performed only if the key is deleted successfully
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

// Check whether it exists
function has(target: object, key: string | symbol) :boolean {
  const result = Reflect.has(target, key)
  BuiltInSymbols is not one of the 12 methods on the Symbol prototype
  if(! isSymbol(key) || ! builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key) }return result
}

// Get an array of all its attributes
function ownKeys(target: object) : (string | symbol) []{
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}
Copy the code

conclusion

Vue3 response type are implemented through a Proxy object Proxy, any action will be mapped to the original object, through the Reflect of operation is usually dependent on collection, modify, add, delete, or trigger an depend on for the operation of the array to intercept, the Proxy is not very good deal with perfect, need to hijack array method, All interception operations are encapsulated in the createReactiveObject function

collectionHandler

Vue3 writes interceptor methods specifically for array collection types, when collectionHandlers. Ts is clicked

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false.false)}export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false.true)}export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(true.false)}export const shallowReadonlyCollectionHandlers: ProxyHandler<
  CollectionTypes
> = {
  get: /*#__PURE__*/ createInstrumentationGetter(true.true)}Copy the code

Vue3 defines only one GET. How does vue3 intercept other operations? This is because the array set type has different operations from other data types: it uses its own API to operate, such as add, get, and so on. These operations are encapsulated in the createInstrumentations function

Why rewrite it

This Set is related to the implementation principle of Map. Both Set and Map internal data are accessed through this, which is called memory slot. When directly accessed through the interface, this points to Set; when accessed through a proxy object, this points to a proxy, which is inaccessible. Explain in detail

Set read operation

function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // #1772: readonly(reactive(Map)) should return readonly + reactive version
  // of the value
  // Target may be: read-only proxy object The raw data may be a mutable proxy object
  // You need to fetch the RAW data of a read-only proxy object (perhaps a mutable proxy object) from react.flags. RAW and then fetch it again with toRaw
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  // Since maps can use objects as keys, there may be proxy objects as keys to get the original key
  const rawKey = toRaw(key)
  // Collect dependencies regardless of whether key and rawKey are the same
  if(key ! == rawKey) { ! isReadonly && track(rawTarget, TrackOpTypes.GET, key) } ! isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)// The has method on the raw data prototype
  const { has } = getProto(rawTarget)
  // Find different methods depending on the reactive API being called
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  if (has.call(rawTarget, key)) {
    // The value for key exists
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    // rawKey exists
    return wrap(target.get(rawKey))
  } else if(target ! == rawTarget) {// #3602 readonly(reactive(Map))
    // ensure that the nested reactive `Map` can do tracking for itself
    // rawKey and key do not exist and the two data are not the same (thus indicating target is a mutable proxy object)
    target.get(key)
  }
}
Copy the code

There is one caveat to this method

If you use readonly(Reactive (Map)) nested with reactive(Reactive (Map)), target may be a proxy object, and if you use readonly(Reactive (Map) nested with reactive(Map)), target may be a proxy object. RawTarget is also a Map object, so let the agent trace it,

The first parameter of the Proxy method should not be confused with the first parameter of the Proxy method. This refers to the Proxy object that has already been proided. The first parameter of the interception method is usually the target original object, and the internal this is usually the Proxy object itself

Set write operation

There are two types of operations to intercept. The Set and add methods, which correspond to Map and Set respectively, are treated differently

  • Set, WeakSet write operation
// Set WeakSet unique
function add(this: SetTypes, value: unknown) {
  // Value can be a proxy object that gets the original value
  value = toRaw(value)
  // Target is a proxy object that needs raw data
  const target = toRaw(this)
  // Get the prototype and use the has method to determine if value exists in target
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  // Add the value if it does not exist and trigger the dependency
  if(! hadKey) { target.add(value) trigger(target, TriggerOpTypes.ADD, value, value) }// Return yourself
  return this
}
Copy the code
  • Map and WeakMap write operations
// Map WeakMap is unique
function set(this: MapTypes, key: unknown, value: unknown) {
  // Value can be a proxy object that gets the original value
  value = toRaw(value)
  // Target is a proxy object that needs raw data
  const target = toRaw(this)
  // The prototype has, get methods
  const { has, get } = getProto(target)

  // Check whether the value exists
  let hadKey = has.call(target, key)
  if(! hadKey) {// There is no retrieving the original key again
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    // It exists, but the rawKey and key are verified to prevent them from obtaining the target incorrectly
    checkIdentityKeys(target, has, key)
  }

  // Get the old value
  const oldValue = get.call(target, key)
  target.set(key, value)
  // The dependency is triggered after the modification does not exist
  if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}
Copy the code

Write operation is relatively simple, the most important thing is to judge whether there is an old value or not, and the triggered dependence is different. Set and WeakSet are modified if they exist, and new if they do not exist, while Map will not rely on them if they exist, and compared with the original object that baseHandler uses Reflect mapping, And here I’m using my own API to do it,

Set iterator

function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function(
    this: IterableCollections, ... args: unknown[]) :可迭代 & Iterator {
    // Target may be: read-only proxy object The raw data may be a mutable proxy object
    // Get the RAW data of the read-only proxy object from react.flags. RAW
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    // Is the target Map type
    const targetIsMap = isMap(rawTarget)
    // Set does not have entries, this is an iteration of Map only called by Map, isPair is true
    // Each iteration returns an array of the form [key value]
    // The following (method === symbol. iterator && targetIsMap) is because the symbol. iterator call triggers entries
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    // Set does not have keys, this is the Map iteration method,
    const isKeyOnly = method === 'keys' && targetIsMap
    // Execute the native iteration method keys values entries
    constinnerIterator = target[method](... args)Warp returns the corresponding method according to the conditions such as toReactive if reactive
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // Not a read-only collection dependency! isReadonly && track( rawTarget, TrackOpTypes.ITERATE, isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY )// return a wrapped iterator which returns observed versions of the
    // values emitted from the real iterator
    // Returns a wrapped iterator whose values are returned by the default iterator
    return {
      // iterator protocol
      next() {
        // Remove two important values: the value of the current iteration and whether the iteration is complete
        const { value, done } = innerIterator.next()
        // If done is true, the value returned is undefined, so there is no need to respond
        return done
          ? { value, done }
          : {
            // If there is a pair of returned arrays where index 0 is key and index 1 is value, it does not mean that WeakSet and WeakSet have only values
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      Iterable protocol returns the iterable object itself
      // A custom iterator returns the iterator itself
      [Symbol.iterator]() {
        return this}}}}Copy the code

Map and Set are different in terms of iteration,

The two images above are the prototypes of Map and Set respectively. There are three differences

1.Set entries and keys call values methods, whereas Map has two separate methods

2.Map Symbol(symbol.iterator) calls entries and Set Symbol(symbol.iterator) calls values().

  1. The two data adding methods are different. A Map can set a key value using the set method, but a set cannot set a key value using the add method

Because of these three differences, the above processing is different. The main logic is that the iterator is wrapped and the value returned by next is processed by the corresponding reactive API each time.

There is another way to iterate

function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function, thisArg? : unknown) {
    // Store the data of the proxy object before it gets the proxy
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    Warp returns the corresponding method according to the conditions such as toReactive if reactive
    constwrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive ! isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)return target.forEach((value: unknown, key: unknown) = > {
      // important: make sure the callback is
      // 1. invoked with the reactive map as `this` and 3rd arg
      // 2. the value received should be a corresponding reactive/readonly.
      // For better traversal, the external function is called from the inside, and the data is read-only or reactive
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}
Copy the code

It’s not logically complicated. The main thing is that it hijacks the method forEach and proxies the data.

conclusion

Due to the underlying design of array collection, it cannot be hijacked by Proxy. Instead, it can only be hijacked by its own interface, which may reflect the hijacking method or map to the original object.

The hijacking method will first get the original object and the raw data passed in, and then the method on the prototype of the original object, bind this to the original object to call.

For GET and HAS, insert the collection dependency logic, and then convert the return value (because HAS returns a Boolean value, do not convert it). The same goes for iterators, but eventually you need to convert the iteration data into a response return

Write operations need to insert logic that triggers dependencies to update

The final summary

BaseHandler; collectionHandler; collectionHandler; collectionHandler