preface

Document overview

Before diving into the source code, let’s take a look at the files in the SRC directory

Finger Exercises ── Finger Exercises ── finger Exercises ── finger Exercises ── finger Exercises ── finger Exercises ── Finger Exercises// The main entry exposes all methods├ ─ ─ operation. Ts// Contains TrackOpTypes and TriggerOpTypes├ ─ ─ reactive. Ts └ ─ ─ ref. TsCopy the code

According to these files, we can divide the responsive module into the following parts: Effect, Reactive, REF, computed and Handlers

We know the difference between Vue3 and Vue2 in the implementation of reactive modules is that they go from defineProperty to proxy, and handlers are the handlers that are passed in when the proxy object is created

Pay attention to the point

We know that the core principles of Vue responsiveness are dependency collection and trigger updates. You learn about effect, Reactive, handlers and you get a sense of what’s going on.

There are a few points you should pay attention to in these modules, such as:

  • effectIn thetrackandtriggerThese two functions rely on collecting and triggering updatesThe core functionIs the most important thing. When you first look at these two functions, you may not understand what they do, but as you move on to the other modules, you will see the light at the end of your eye.
  • handlersIn thegetandsetA lot of things are done inside these two functions, which also rely on collecting and triggering updatesThe main entrance

effect

effect

When we normally use Effect, we pass in a callback function that listens for reactive data inside the function and executes it every time the reactive data is updated. Such as:

const num = ref(1) // Reactive data
effect(() = > console.log(num.value)) // effect

// When reactive data is updated, the callback function is triggered
num.value = 2 // console.log(2)
Copy the code

How is this implemented internally? Now let’s look at the source code for Effect

export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  // If fn is effect, fetch the original value
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options) Call createReactiveEffect to create an effect

  // If it is not lazy, effect is executed immediately
  if(! options.lazy) { effect() }return effect
}

Copy the code

As you can see, this function receives two main parameters, one is the callback function (fn), and the other is options

// options

exportinterface ReactiveEffectOptions { lazy? : boolean// Whether to delay triggering effect. Normally, it is triggered once before data is updatedscheduler? :(job: ReactiveEffect) = > void // Scheduling functiononTrack? :(event: DebuggerEvent) = > void / / to monitor trackonTrigger? :(event: DebuggerEvent) = > void / / to monitor the triggeronStop? :() = > void // Triggered when listening is stoppedallowRecurse? : boolean }Copy the code

Internally, Effect does these things:

  • judgefnIf and yeseffect, then extract its original value
  • callcreateReactiveEffectcreateeffect
  • ifoptionsIs not set inlazy“Is executed immediatelyeffectFunction, and finally returnseffectfunction

We can see that the core of this is creating an effect by calling createReactiveEffect

Now let’s look at this function

function createReactiveEffect<T = any> (
  fn: () => T, // The callback function passed in earlier
  options: ReactiveEffectOptions // Previous options
) :ReactiveEffect<T> {
  const effect = function reactiveEffect() :unknown {
    // If effect is not active, which happens after we call the stop method in effect,
    // If no scheduler function was previously called, call the original method fn, otherwise return.
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    if(! effectStack.includes(effect)) { cleanup(effect)// Clear dependencies: current effect deps. Avoid relying on duplicate collection
      try {
        enableTracking() // It can be tracked, which is used to trigger the judgment of track
        effectStack.push(effect) // Push to the stack, indicating that it is in the state of collecting dependencies
        activeEffect = effect // To collect dependencies
        return fn() // Execute the callback function for dependency collection
      } finally {
        effectStack.pop() // Pop the stack to exit the dependency collection state
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // return to original state}}}as ReactiveEffect

  // Mount propertieseffect.id = uid++ effect.allowRecurse = !! options.allowRecurse// Allow recursion
  effect._isEffect = true
  effect.active = true // Whether to activate
  effect.raw = fn
  effect.deps = [] // Hold the current effect dependency (DEP) array
  effect.options = options
  return effect
}
Copy the code

CreateReactiveEffect takes the same two parameters as effect.

It does these things internally:

  • To define aeffectfunction
  • In thiseffectFunction to mount properties
  • Return to theeffectFunction, i.e.,const effect = createReactiveEffect(fn, options)The left side of the

Let’s take a look at the properties it mounts and what’s going on inside the effect function

effect.id = uid++ // uid indicates the effect numbereffect.allowRecurse = !! options.allowRecurse effect._isEffect =true // Indicates whether it is effect
effect.active = true // Whether it is in the active state
effect.raw = fn // save the original function
effect.deps = [] // Hold the current effect dependency (DEP) array
effect.options = options // Save passing in the second argument, options
Copy the code

The important thing to watch out foreffect.depsAnd this is related to what we’re going to talk abouttrackThe contents of a function are closely related

Let’s go back to the newly defined Effect function

const effect = function reactiveEffect() :unknown {
    // If effect is not active, which happens after we call the stop method in effect,
    // If no scheduler function was previously called, call the original method fn, otherwise return.
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    if(! effectStack.includes(effect)) { cleanup(effect)// Clear dependencies: current effect deps. Avoid relying on duplicate collection
      try {
        enableTracking() // It can be tracked, which is used to trigger the judgment of track
        effectStack.push(effect) // Push to the stack, indicating that it is in the state of collecting dependencies
        activeEffect = effect // To collect dependencies
        return fn() // Execute the callback function
      } finally {
        effectStack.pop() // Pop the stack to exit the dependency collection state
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] ActiveEffect Restore to original state}}}as ReactiveEffect
Copy the code

This function basically does these things internally:

  • If the currenteffectNot active, that is, already activestopIf the previousoptionsThere is noschedulerFunction, then call it directlyfn(callback function), otherwise returns directly.
  • ifeffectStackDoes not contain the currenteffect, just callcleanupClears previous dependencies, i.eeffect.deps
function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0}}Copy the code
  • And then you start coming inPreparing for dependency collectionState, callenableTracking, will the currenteffectpusheffectStackandThe currenteffectSet toactiveEffect(This step is important, which is closely related to the later call of the track function)And then callfn(Depending on the actual execution of the collection, which will be covered later in proxy-Handler)
  • Finally will the currenteffectOut of the stack, callresetTracking.activeEffectRestitution.

We saw that enableTracking and resetTracking were called in the above step. What does that do?

let shouldTrack = true
const trackStack: boolean[] = []

export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}
Copy the code

We can see that all three of these functions operate on shouldTrack and trackStack, and it’s important to note that the value of shouldTrack is also an important factor in dependency collection, which we’ll talk about in a minute.

Conclusion:

  • effectThe function’s main job is to create oneeffectFunction, on which a number of properties are mounted, for exampledeps,active,options. When creating aeffectThe function also handles some cases, such as settinglazyTo avoid calling it at first; There will beeffectThe function is passed in as a callback again.
  • Defined internallyeffectFunctions are mainly for dependency collection later on. There are a few things to note about dependency collection:effect.deps,activeEffect,shouldTrack

track

When you read about the track and trigger functions, you might wonder why they are core because they don’t exist inside effect. Indeed, these two functions do not appear in Effect and are mainly called by handlers and refs in proxy objects created by Reactive, and so on. This is the channel through which other modules communicate with Effect.

Now when you look at these functions you’ll probably be half-solved, but when you read the proxy handler, the actual call to the function, you’ll get the point.

Now without further ado, let’s move on to track

The first thing to know is that in the Effect module, a targetMap is maintained to hold the dependencies of each object

const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // Do not collect dependencies
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  // Start relying on collections
  // Get the depsmap of the trigger object
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the dependency set of depsmap keys
  let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) { dep.add(activeEffect)// Add activeEffect to dependency set
    activeEffect.deps.push(dep) // Add set to the activeEffect DEP
                      
    // Execute onTrack if there is an onTrack function for the development environment and activeEffect
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
Copy the code

First look at the parameters (target: object, type: TrackOpTypes, key: unknown), then the trigger object, then the track operation (get/has/iterate), then the key of the trigger object.

Here’s what’s going on inside:

  • checkshouldTrackandactiveEffectThese are two properties that we already have ineffectYes, I have. whenshouldTrackforfalseOr noactiveEffect, dependency collection is not performed
  • Began to gettargetthedepsMap, if no, create it
  • fromdepsMapTo obtain the correspondingkey(Attribute) dependencyA collection of(deP), if not, create it
  • After owning a dependency collection of attributes of the current object, if the collection does not containactiveEffect, it willactiveEffectAdd to the dependency set (DEP), and add to the dependency setactiveEffect.deps
  • Finally, if the development environment andactiveEffect.optionsSet in theonTrackIs called

Conclusion: The main function of this function is to add effect to the target object’s dependency collection (DEP).

trigger

In the track function, we know that dependencies are collected in targetMap. The trigger function is the function that notifies dependencies when listening properties are updated.

export function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  // The object is not traced, and is returned if there are no dependencies on the object
  const depsMap = targetMap.get(target)
  if(! depsMap) {return
  }

  const effects = new Set<ReactiveEffect>() // effects Set
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }// Different operations depending on the type --> add the appropriate DEP to effects
  // CLEAR
  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) { 
   	// If target is an array, the array length has changed (shortened)
    // add the 'length' and key>=newValue dependency
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }

    switch (type) {
      // ADD
      case TriggerOpTypes.ADD:
        // If it is not an array
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))// If map is used
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // The array becomes longer
          // new index added to array -> length changes
          add(depsMap.get('length'))}break
      // DELETE
      case TriggerOpTypes.DELETE:
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      // SET
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break}}const run = (effect: ReactiveEffect) = > {
    // If trigger is listened on in effect.options
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // If there is a scheduling function
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect() / / execution effect
    }
  }

  effects.forEach(run) // Execute all the collected effects
}
Copy the code

Let’s look at the parameters that the function receives: Target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? : Map

| Set

,>

Type :TriggerOpTypes (clear, set, delete, add) TriggerOpTypes (clear, set, delete, add) TriggerOpTypes (clear, set, delete, add)

Let’s take a quick look at what this function does and then take a closer look at it:

  • Maintain aeffectsCollection for storing to be performedDistributed updateThe dependence of
  • According to thetriggerThe operation is different toeffectsAdd the qualified to the seteffect
  • traverseeffectsCollection, executes each of the collectionseffect

Now for a closer look:

  • We know that all dependencies are stored againtargetMapSo at the beginning, we definitely want to gettargetIf not found, it means that there is no dependency, and there is no need to distribute updates
const depsMap = targetMap.get(target)
if(! depsMap) {return
}
Copy the code
  • Next, start maintaining oneeffectsCollection, and also provides aaddThe function adds dependencies to the collection
const effects = new Set<ReactiveEffect>() // effects Set

const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
   if (effectsToAdd) {
     effectsToAdd.forEach(effect= > {
			 // Add to Effects if effect is not currently active (not collecting dependencies) or allowRecurse is set
       if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }Copy the code
  • Depending on the operation typeeffectsAdd a dependency to:
    • whentypeforclear.targetthedepsMapAll add
    • whentargetFor arrays,keyforlengthSaid,The array length has changedThat will belengthandkeyItems larger than the new length are added
    • whentypeforaddIf thetargetNot an array, will betargettheITERATE_KEYThe dependency set joins, and iftargetismap, will be righttargettheMAP_KEY_ITERATE_KEYThe dependency collection is added. iftargetIt’s an array, and if the array gets longer, it’s going to betargetthelengthDependency collection joinseffects
    • whentypefordeleteIf thetargetNot an array, will betargettheITERATE_KEYThe dependency set joins, and iftargetismap, will be righttargettheMAP_KEY_ITERATE_KEYThe dependency collection is added.
    • whentypeforsetIf thetargetismapTo havetargettheITERATE_KEYThe dependency collection is added
  • In the previous step, we added the dependencies that need to be distributed as updates. Now we can officially distribute updates: effects.forEach(run).runThe logic of a function is simple: executeeffect.

If you remember what effect is, let’s review it:

const effect = function reactiveEffect() :unknown {
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    if(! effectStack.includes(effect)) { cleanup(effect)// Clear dependencies: current effect deps. Avoid relying on duplicate collection
      try {
        enableTracking() // It can be tracked, which is used to trigger the judgment of track
        effectStack.push(effect) // Push to the stack, indicating that it is in the state of collecting dependencies
        activeEffect = effect // To collect dependencies
        return fn() // Execute the callback function
      } finally {
        effectStack.pop() // Pop the stack to exit the dependency collection state
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // return to original state}}}as ReactiveEffect
Copy the code

By this point you may be aware that dependency collection is re-implemented when updates are distributed, and the FN function is executed again

reactive

reactive

Const obj = reactive({name:’obj’}) const obj = reactive({name:’obj’});

Now let’s get into the source code

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // If the object is read-only, it is returned directly
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers, // Hijacking of common reference data types (object/array)
    mutableCollectionHandlers // Set /map/WeakMap/WeakSet)}Copy the code

Old convention, look at parameters: target:object. You pass in an object.

Now let’s see what happens inside the function:

  • If the object is read-only, it cannot be proxied and returned directly
  • And then return to callcreateReactiveObjectFunction, which takes four arguments.

Now that the core implementation is in this function, let’s see what this function does.

function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler
       
        , collectionHandlers: ProxyHandler
        
       ) {
  // If it is not an object, return it directly
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
  
  // The object is already proxy
  // Proxy is reactive (there are two types of proxy, one is read-only, the other is reactive)
  if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
  }
  // target already has corresponding Proxy
  // If a proxy is already mounted to the object, the proxy is returned
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  / / object types in the white list (object/array/map/set/weakmap/weakset) would be hijacked
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  / / create the proxy
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
Copy the code

Let’s see what the four parameters we just passed in correspond to:

target  ->  target: Target / / the original object
false   ->  isReadonly: boolean // Whether it is read-only
mutableHandlers  ->  baseHandlers: ProxyHandler<any> // handlers
mutableCollectionHandlers  ->  collectionHandlers: ProxyHandler<any> // handlers
Copy the code

The first two are easy to understand, but what are the handlers? We know that new Proxy (Target, Handle) syntax looks like this. The two handlers are passed in for later proxy creation. The details of the handler will be covered later, which is also the key part.

Let’s get inside the function formally:

  • Target is evaluated first and returns if target is not an object

  • Then if the target passed in is already a proxy and is not read-only or responsive, it returns directly

  • IsReadonly: Boolean (); if the target has already created a proxy object, return the previously created proxy

    export const reactiveMap = new WeakMap<Target, any>() // Cache Map of normal type
    export const readonlyMap = new WeakMap<Target, any>() // Cache Map of read-only type
    Copy the code
  • At this point, target does not create a proxy. The next step is to get the type of target, since we pass in a different handler** based on the type to create the proxy object, and return target directly if the type is not available. The function to get the type is as follows:

    const enum TargetType {
      INVALID = 0./ / is not available
      COMMON = 1.// Common type, Object/Array
      COLLECTION = 2 // Set type, Map/Set/WeakMap/WeakSet
    }
    
    // Whitelist object type
    function targetTypeMap(rawType: string) {
      switch (rawType) {
        case 'Object':
        case 'Array':
          return TargetType.COMMON
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
          return TargetType.COLLECTION
        default:
          return TargetType.INVALID
      }
    }
    
    // Get the type of the target object
    function getTargetType(value: Target) {
      return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
        ? TargetType.INVALID
        : targetTypeMap(toRawType(value))
    }
    Copy the code
  • The last step is to create a proxy object by passing in different Handlers based on the type of target. It then caches and returns the proxy object.

Reactive creates a reactive object by creating different proxies for the target type and handling special cases where the object passed in is not an object but is already a proxy.

Dependency on collecting and distributing updates is not mentioned in this section because the core secret is in the Handlers. In the next section we will uncover the Handlers.

proxy-handler

As you will recall, Reacitve creates a proxy object and passes handlers based on the type of target. Target is mainly divided into common (including Object/Array) and Collection (including Map/Set/WeakMap/WeakSet). Therefore, corresponding handlers are divided into two categories, namely baseHandlers and collectionHandlers

baseHandlers

There are four handlers in the baseHanlers category, namely mutableHandlers, readonlyHandlers, shallowReactiveHandlers, and shallowReadonlyHandlers.

The main thing we’re going to do is mutableHandlers, because when we call reactive, the third parameter is called mutableHanlers. You can go back to the previous section to see the code.

The other three are for other apis provided in Reactive, such as shallowReactive, ReadOnly, and shallowReadonly related handlers.

Now let’s look at mutableHandlers:

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

You can see that the contents are actually 5 related operation functions. Compared to defineProperty, which only overwrites GET and set, proxy extends many functions.

The other three Handlers contain much the same content. The difference is that, for example, get calls a factory function, while the handlers call a factory function with different parameters:

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

Now let’s look at these five actions in detail.

get

As you can see above, get is created by calling createGetter. So let’s look at this function:

function createGetter(isReadonly = false, shallow = false) {
  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) // Check whether target is an array

    ArrayInstrumentations is called if target is an array and not read-only, and key is an overwritten array method
    ArrayInstrumentations is a rewrite of the arrayInstrumentations method, which obtains res from it
    if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver) 
    }

    const res = Reflect.get(target, key, receiver) / / get the value

    // If the attribute is of type symbol
    // Then the attribute of symbol itself (in the set) or key is __proto__/_v_isRef
    // Return the result directly (no dependency collection)
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : isNonTrackableKeys(key) / / the key for __proto__ / _v_isRef
    ) {
      return res
    }

    // Dependency collection is performed in non-read-only cases
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }// If it is shallow, return res
    if (shallow) {
      return res
    }

    // If it is an object of type ref, return ref.value
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }

    // If it is an object, it is recursively processed to return a responsive object
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
Copy the code

First, take a look at the parameters (isReadonly = false, shallow = false), which are marked read-only and shallow responsive. This is mainly due to the differences in the two parameters passed in by different Handlers when creating get. Then look at the return value, which returns a GET function when the factory function is called.

Now let’s focus on what happens inside the GET function.

  • The special key is processed first. IS_REACTIVE returns when the key of the target object to be accessed is reactiveFlags. IS_REACTIVE! IsReadonly; IS_READONLY returns isReadonly if reactiveFlags. IS_READONLY; Or reactiveFlags. RAW, and the receiver is a proxy for the target object stored in Map (from Reactive)

  • Then determine whether target is an array. If it is an array and the key accessed is an array method, the instrumentations method is obtained from the arrayInstrumentations and called (Reflect.get(arrayInstrumentations, key, receiver)). [arrayInstrumentations] [instrumentations] [arrayInstrumentations]

    const arrayInstrumentations: Record<string, Function> = {} // An object containing an overwritten array method
    
    // Rewrite and add the method to arrayInstrumentations by iterating. The overridden method triggers dependency collection internally.; (['includes'.'indexOf'.'lastIndexOf'] as const).forEach(key= > {
      const method = Array.prototype[key] as any // Save the original method
      // Rewrite the method and add it to arrayInstrumentations
      arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
        const arr = toRaw(this) // Get the source array from which the method was called
        for (let i = 0, l = this.length; i < l; i++) {
          track(arr, TrackOpTypes.GET, i + ' ') // Collect dependencies on each item of the array
        }
      
        const res = method.apply(arr, args) // Call the original method
        if (res === -1 || res === false) {
          return method.apply(arr, args.map(toRaw)) // If the call fails, re-call with the original value
        } else {
          returnres } } }) ; (['push'.'pop'.'shift'.'unshift'.'splice'] as const).forEach(key= > {
      const method = Array.prototype[key] as any // Save the original method
      
      // Rewrite the method and add it to arrayInstrumentations
      arrayInstrumentations[key] = function(this: unknown[], ... args: unknown[]) {
        pauseTracking() // Change the state shouldTrack and trackStack, from effect
        const res = method.apply(this, args) // Call the original method
        resetTracking() // Reset the shouldTrack state and trackStack, from effect
        return res
      }
    })
    Copy the code
  • Continue to return to the process. If target is not an array and key is not in the list of overwritten methods. Const res = reflect. get(target, key, receiver).

  • Once the value is retrieved, handle the special case: return the value directly if the key is a symbol and is an attribute of the symbol itself or if the key is __proto__,__v_isRef,__isVue. Because when these two conditions are met, there is no need for dependency collection.

  • It then starts operating on the parameters passed in to the factory function. If it is not read-only, track is called for dependency collection. Then, if it is shallow, it returns the value directly.

  • If res is a ref, return ref. Value. When an RES is an object, it is wrapped in Reactive/Readonly and returned (this is key for deep listening). If neither of these is the case, an RES is normally returned.

trigger

Let’s review the normal use of effect and Reactive again.

let obj = reactive({name:'obj'}) // Create a reactive object using Reactive
effect(() = > console.log(obj.name)) // Add a dependency to this reactive object
Copy the code

We know that reactive is all about creating a proxy object. When different operations are performed on this object, different Handles are fired.

Let’s review the core code in Effect:

const effect = function reactiveEffect() :unknown {
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    if(! effectStack.includes(effect)) { cleanup(effect)try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn() // Execute the callback function
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
Copy the code

Briefly, when this function is called, previous dependencies are cleaned up. The dependency collection preparation is then started, the callback function fn is called, and the original state is restored.

And we know that dependency collection is not triggered when reactive objects are created, but only when the reactive properties are accessed (get). And there is no place to call track in effect. So how does dependency collection work?

In fact, the key point is the FN function in effect: In the example above, the callback passed in when effect is called is () => console.log(obj.name). Inside this function, the property of the reactive object (obj.name) is accessed. When FN is executed in Effect, the get operation for the corresponding key of the reactive object is triggered. Track is called inside get. So dependency collection is done.

To summarize: When an effect callback is passed in with a relationship to a property of a reactive object, then when the effect function is executed, the callback is also executed inside it, thus triggering the get operation of the reactive object, and track is called inside the GET operation. Hence dependency collection. So the actual dependency collection in effect is in the call to FN.

set

Set, like GET, is created by a factory function. The factory function is createSetter

function createSetter(shallow = false) {
  return function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
    const oldValue = (target as any)[key] // Get the old value

    // Non-shallow response, that is, under normal conditions
    if(! shallow) { value = toRaw(value)If target is not an array and the old value is of type ref, the new value is not of type ref
      if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = value// Assign the new value to the value of the old value (ref type) and let the old value handle the trigger
        return true}}else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    // If the target is an array and the key is a number, check whether the key exists by subscript or hasOwn
    // hadKey: For subsequent trigger update operations, determine whether to add or modify
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)

    const result = Reflect.set(target, key, value, receiver) / / set the value
    // don't trigger if target is something up in the prototype chain of original
    // Trigger when target is not a value on the prototype chain.
    // Because when target is a value on the prototype chain, the operation to set the value is applied to receiver and not target, the target should not trigger an update
    if (target === toRaw(receiver)) {
      // If the key is not present in target, trigger is called to trigger add
      // Or trigger is invoked to trigger the set operation, that is, modify
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value)/ / the add operation
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) / / set operation}}return result
  }
}
Copy the code

As usual, first look at the shallow = false parameter, which, like createGetter, is mainly used to differentiate gets created by different Handlers. Again, the return value is a GET function.

Now take a look at what’s going on inside:

  • The old values are saved first.
  • When is not shallow response, iftargetIs not an array, and then the old value isrefType but the new value is notrefType. Assigns the new value to the old value (oldValue.value = value). Equivalent to letrefTo implement the distribution of updates.
  • And then determinetargetOn whether or notkeyTo define thehadKey. This is to confirm that the operation isAdd or modify. For easy invocationtriggerThe correct operation type can be passed in when an update is dispatched.
  • throughconst result = Reflect.set(target, key, value, receiver)Set the value.
  • Let’s start sending out updates. whentargetIs not a value on the prototype chain, as previously obtainedhadKeyTo do something differenttrigger. When it is new:trigger(target, TriggerOpTypes.ADD, key, value)When modified:trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  • returnresult

deleteProperty

function deleteProperty(target: object, key: string | symbol) :boolean {
  const hadKey = hasOwn(target, key) // Whether it is an attribute of the object itself
  const oldValue = (target as any)[key] // Save the old value
  const result = Reflect.deleteProperty(target, key)
  // Trigger the update
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
Copy the code

Unlike get/set, deleteProperty is not generated by a factory function. So let’s get straight to what’s going on inside:

  • To determinekeyDoes it existtarget
  • Save the old value
  • callconst result = Reflect.deleteProperty(target, key)Carry on formallydeletePropertyOperation and save whether the deletion was successful
  • If the deletion is successful andkeyistargetTo distribute updates totrigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  • The delete result is returned

has

function has(target: object, key: string | symbol) :boolean {
  const result = Reflect.has(target, key)
  // If the attribute is not symbol or an attribute of symbol itself, rely on the collection
  if(! isSymbol(key) || ! builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key) }return result
}
Copy the code

As above, look directly inside the function:

  • Direct callconst result = Reflect.has(target, key)Save the query results
  • If the property is notsymbolorsymbolProperty itself when calledtrackDo dependency collection
  • Returns the result

ownKeys

function ownKeys(target: object) : (string | number | symbol) []{
  // Rely on collection
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}
Copy the code

This function is simple: track collects dependencies and calls reflect.ownkeys (target) to return the result

collectionHandlers

ref

ref

Like Reactive, the REF method is used to create a reactive object. The difference, however, is that the values passed by ref are generally primitive data types rather than reference data types. And.value is used whenever a reactive object created by ref is accessed.

Now let’s dig into the source code and see its secrets

export function ref(value? : unknown) {
  return createRef(value)
}
Copy the code

Like Reactive, it’s very simple inside, calling another function to create an object. Let’s take a look at createRef

function createRef(rawValue: Shallow = false) {// If value has been shallow, Return rawValue {return rawValue} return new RefImpl(rawValue, shallow)}Copy the code

This function is also simpler. It first evaluates the value passed in. If the value is already ref, it returns it, otherwise it calls new RefImpl() to create a new REF object

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue) // Non-shallow, call convert and pass value
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value') // Rely on collection
    return this._value
  }

  set value(newVal) {
    // Check whether the value is updated
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) // Trigger the update}}}Copy the code

First let’s look at its constructor:

constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue) 
}
Copy the code

The constructor takes two parameters (_rawVlaue, _shallow), the initial value and whether it is shallow. The constructor then basically assigns the initial value passed to _value. When shallow, the assignment is direct, otherwise convert(_rawValue) is called and the return value is assigned to _value.

const convert = <T extends unknown>(val: T): T => isObject(val) ? Reactive (val) : val // If the object is reactive proxyCopy the code

Convert is simpler, which simply returns the value wrapped in reactive if the value passed in is an object, and val otherwise. So when ref is passed in, you’re actually calling reactive internally

Let’s look at the other two methods:

get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value') // Rely on collection
    return this._value
  }

set value(newVal) {
    // Check whether the value is updated
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) // Trigger the update}}Copy the code

As you can see, it’s all on value, so now you can see why the method ref has to go through dot value.

  • getInside is very simple, is calledtrackDo the dependency collection, and return the value
  • setWhen the value is updated, the new value is assigned to the original value and the operation in the constructor is repeatedthis._value = this._shallow ? newVal : convert(newVal). The last calltriggerDistribute updates.

So ref is done, and you can see the difference between reactive and reactive in addition to the things I mentioned earlier. There are also these differences:

  • withreactiveThe object created is aproxyAnd therefIt isn’t.
  • reactiveThe location for dependency collection and distribution of updates is inhandlersIn the proxy method, andrefIs in theget/set.

computed

Before entering the source code, it is necessary to see how it is used

const state = reactive({name:'obj'.age:18})

const computedAge = computed(() = > state.age + 10)
const computedName = computed({
  get() {
     return state.name + '123'
  },
  set(val) {
     state.name = val 
  }
})
Copy the code

In normal use, we pass in a callback function, but computed also accepts objects that contain get and set.

Now let’s go into the source code to find out:

export function computed<T> (() {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  Getters and setters are generated from the passed function/objects containing get and set
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions // Getter assigns a value to a function
    setter = __DEV__
      ? () = > {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  / / create ComputedRefImpl
  return newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set// True when a function is passed in
  ) as any
}
Copy the code

First see parameters: (getterOrOptions: ComputedGetter < T > | WritableComputedOptions < T >), in the above mentioned, we can accept a function or a with get and set object.

There are two main things that the function does inside:

  • From the parameters passed ingetter/setter. When a function is passed in, the function is assigned togetter; When an object is passed in, the corresponding willget/setAssigned togetter/setter
  • callComputedRefImplInstantiate an object and return it.

The focus is on the computedRefImpl class:

class ComputedRefImpl<T> { private _value! : T private _dirty = true public readonly effect: ReactiveEffect<T> public readonly __v_isRef = true; Public readonly [reactiveflags. IS_READONLY]: Boolean constructor(getter: ComputedGetter<T>, private readonly _setter: ComputedSetter<T>, isReadonly: Boolean) {// This. Effect = effect(getter, {lazy: true, scheduler: scheduler); () => { if (! This._dirty) {this._dirty = true trigger(toRaw(this), triggeroptypes.set, 'value') // All computed dependencies are updated}}}) this[reactiveFlags.is_readOnly] = isReadonly // Set flag} get value() {// If if a dependency changes (this._dirty) {this._value = this.effect()} track(toRaw(this), Trackoptypes.get, 'value') // Collect computed dependencies return this._value} set value(newValue: T) {this._setter(newValue)}}Copy the code

Let’s look at the included attributes first:

private _value! : T/ / value
private _dirty = true // Flag whether to cache

public readonly effect: ReactiveEffect<T> / / rely on

public readonly __v_isRef = true; // Identifies the ref object
public readonly [ReactiveFlags.IS_READONLY]: boolean // reactive Flag
Copy the code

You can see the familiar effect appearing in the property. In fact, the heart of computed is that it uses effect internally to add dependencies, so it has the same effect as effect. There is also a **_dirty attribute, which is one of the keys for computed cache updates **.

Let’s move on to the constructor:

constructor(getter: ComputedGetter
       
        , private readonly _setter: ComputedSetter
        
         , isReadonly: boolean
        
       ) {
    // Call the effect method to wrap the getter passed in as a response
    // The object behind it is options
   this.effect = effect(getter, {
      lazy: true.scheduler: () = > {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value') // Trigger the update}}})this[ReactiveFlags.IS_READONLY] = isReadonly / / set the flag
}
Copy the code

There are three parameters: getter, settter, and isReadonly. We also saw in the computed function that we just did that there are two things that actually get passed in:

  • When the incomingcomputedIs a function, then passed inComputedRefImplThe arguments to the constructor correspond to a callback function, a prompt function, andtrue
  • When passed in is a stringget/setThe constructor argument is passed to the corresponding: objectget, objectset,false

What the constructor does is simple: it creates an effect and assigns it to this.effect, which is cached (this is one of the differences, computed does caching). The focus is on the parameters passed in when you create an effect. The first argument is the getter, and the second argument we know is an options object that contains lazy and a scheduler scheduling function.

function effect<T = any> () :ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options) / / create the effect
  // If it is not lazy, effect is executed immediately
  if(! options.lazy) { effect() }return effect
}
Copy the code

The effect of lazy is known from the effect source code. An effect is not executed immediately after it is created. The scheduler is called by the run function inside the trigger.

export function trigger() {
  const run = (effect: ReactiveEffect) = > {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect) // The scheduling function is called
    } else {
      effect()
    }
  }

  effects.forEach(run)
}
Copy the code

After each trigger, the scheduler function is called. In this case, the scheduler is mainly set to _dirty and triggers the trigger function (which dispatches updates to all dependencies on computed)

Now let’s see get value()/set value() :

Getvalue () {if (this._dirty) {this._value = this.effect()} getvalue () {this._dirty = false} Track (toRaw(this), trackoptypes.get, 'value') return this._value} set value(newValue: T) { this._setter(newValue) }Copy the code
  • set: when thecomputedIs called when the value is set directlysetter. If the callback. withcomputedWhen a function is passed in, the set warning function is called. Otherwise, the incoming object’sset
  • whilegetFirst of all, yes_dirtyAttribute to determine. if_dirtyfortrue, the dependency has changed and therefore needs to be calledthis.effectGet the latest value and modify it_dirty. And then calltrackDoes the dependency collection and returns the value

You can see that the key is the change to this._dirty. When getter dependent reactive data is updated, the set scheduler scheduler function is called and _dirty is set to true to indicate that the data has changed. Then, when the computed value is accessed again, this.effect is called again to get the new value and the _dirty value is restored to false.