preface

  • Finally, we arrived at the long-cherished Effect, which took a lot of effort. This chapter mainly analyzes how to create listener function in three parts: createReactiveEffect; Track how to collect listener functions (dependent collection); And how trigger triggers listener functions to perform dependent updates. These three parts are the core of this article. Once you know what effect is, you will know what effect is.
  • Effect source code portal

Creating a listener function

We’ll start by creating effect, but we’ll need to combine two external calling functions as entry points: the familiar watchEffect, Watch. If you want to ask me why I started with them instead of the effect function directly, give you a look and feel for yourself. Oh, almost forgot, these two methods on the packages/vue/dist/vue. Global. Js find, if found himself projects don’t have the catalog file, then NPM run dev run under the project.

Let’s find out where these guys are

watchEffect/watch

// Simple effect.
function watchEffect(effect, options) {
  return doWatch(effect, null, options)
}
// initial value for watchers to trigger on undefined initial values
const INITIAL_WATCHER_VALUE = {}
// implementation
function watch(source, cb, options) {
  if(! isFunction(cb)) { warn(`\`watch(fn, options?) \` signature has been moved to a separate API. ` +
        `Use \`watchEffect(fn, options?) \` instead. \`watch\` now only ` +
        `supports \`watch(source, cb, options?) signature.`)}return doWatch(source, cb, options)
}
Copy the code

Let’s take a look at the above code:

  • Both functions are called internallydoWatchFunction, and returns the result of the function’s execution, as we can guess by now if we have seen the official documentationdoWatchThe function returns a function that stops listening.
  • watchEffectThere is nocbCallback, so the first argument is both the original function and the side function,watchcbCallbacks are side effects
  • watchcbIf the parameter is not a function type, a warning is raised, but no error is reportedcbIt can be any other type, but it’s usually better to write it as a function.

Then let’s go to The Inside of doWatch. It looks very long (don’t get your head around it) and watch it in sections.

doWatch

function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ, instance = currentInstance) {
if(! cb) {if(immediate ! = =undefined) {
    warn(
      `watch() "immediate" option is only respected when using the ` +
        `watch(source, callback, options?) signature.`)}if(deep ! = =undefined) {
    warn(
      `watch() "deep" option is only respected when using the ` +
        `watch(source, callback, options?) signature.`)}}const warnInvalidSource = s= > {
  warn(
    `Invalid watch source: `,
    s,
    `A watch source can only be a getter/effect function, a ref, ` +
      `a reactive object, or an array of these types.`)}... }Copy the code

If cb does not exist, it is watchEffect. If cb does not exist, it is watchEffect. If cb does not exist, it is watchEffect. These two properties are not recommended (not supported) as specified in the watchEffect method.

function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ, instance = currentInstance) {...let getter;// Pass to the 'effect' method as the first argument (data source function fn)
      let forceTrigger = false;// Is whether to force the 'cb' side effect function when the data source in the 'watch' method is of type Ref
      // Determine the data type of the data source 'source'
      // If the data source is a Ref, a getter is a function that returns the value of a solved Ref
      if (isRef(source)) {
          getter = () = >source.value; forceTrigger = !! source._shallow;// Whether to enforce the switch of the side effect function
      }
      // If the data source is a reactive object, a getter is a function that returns that reactive object
      else if (isReactive(source)) {
          getter = () = > source;
          deep = true;// Deep listening switch
      }
      // If the data source is an array, a getter is a function that iterates through each element of the array, evaluates the type of each element, and returns the result as a new array element.
      // Watch's datasource that listens for multiple data sources is executed here, and the new array returned is passed to cb (side effect function) as the first argument
      else if (isArray(source)) {
          getter = () = > source.map(s= > {
              if (isRef(s)) {
                  return s.value;
              }
              else if (isReactive(s)) {
                  return traverse(s);
              }
              else if (isFunction(s)) {
                  return callWithErrorHandling(s, instance, 2 /* WATCH_GETTER */);
              }
              else{ warnInvalidSource(s); }}); }If cb exists, the getter is initialized. If cb exists, the getter is executed. The getter is a function that returns the latest dependent data
    // If watchEffect is not present, the getter is both the data source function and the side effect function
      else if (isFunction(source)) {
          if (cb) {
              // getter with cb
              getter = () = > callWithErrorHandling(source, instance, 2 /* WATCH_GETTER */);
          }
          else {
              // no cb -> simple effect
              getter = () = > {
                  if (instance && instance.isUnmounted) {
                      return;
                  }
                  if (cleanup) {
                      cleanup();
                  }
                  return callWithErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onInvalidate]); }; }}// If none of the above data types match, the data source passed in is an invalid value
      else {
          getter = NOOP;To avoid errors, the getter is given a default function
           warnInvalidSource(source);
      }
      If cb is present and deep is true, the watch function is executed and the data source is a responsive object type
      The gette function is a function that performs the deep listening and collection of attributes and returns the responsive object
       if (cb && deep) {
          const baseGetter = getter;
          getter = () = >traverse(baseGetter()); }}... }Copy the code

First we define two variables getters, forceTrigger. The getter is passed to the effect method as the first argument (the data source function fn), and the forceTrigger is the switch that forces the CB side effect function in the watch method if the data source is of type Ref. Then come down to judge the data type of the data source source:

  • If the data source isRefThe type,getterIs one that returns unnestedrefThe function of
  • If the data source is a reactive objectgetterIs a function that returns the reactive object
  • If the data source is an arraygetterIs a function that iterates through each element of an array, evaluates the type of each element, and returns the result as a new array element.watchData sources that listen for multiple data source schemas are executed here, and the new array returned is passed tocb(Side effect function) as the first argument)
  • If the data source is a function, it dividescbWhether there are two cases of initializationgetterIf thecbTo exist is to executewatchThe function comes in,getterIs a function that returns the latest dependent data; If it does not exist, it is executedwatchEffectComing in, right nowgetterBoth a data source function and a side effect function.

If none of the above data type judgments match, then the data source passed in is an invalid value. Call warnInvalidSource to warn. If cb exists and deep is true, the watch function is executed and the data source is a responsive object type, then the internal properties of the data source will be deeply listened (the properties of the object type will be collected into a set). A getter function is a function that performs property deep listening and collection and returns the reactive object.

function doWatch(
  source,cb,{ immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,instance = currentInstance
) {...let cleanup // Clean up any side effects that are still in effect from the last side effect function.
  // Registers an invalidation callback that cleans up the side effects left over from the last time the side effect function was executed
  The fn argument is a user-defined cleanup function that is executed each time the side effect function is re-executed or the current component is unloaded
  const onInvalidate = fn= > {
    //onStop is remounted to the Options configuration object of the Effect listener each time and is called the next time the side effect function executes
    cleanup = runner.options.onStop = () = > {
      callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */)}}// Initialize the old value
  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
  // The job function is a task function that performs side effects
  const job = () = > {
    // Runner is a listener function. If the active attribute is false, the listener is stopped and returns without performing any side effects
    if(! runner.active) {return
    }
    // Cb is the side effect function in the watch method. If it exists, execute it (side effect). If it does not, execute runner (effect itself)
    if (cb) {
      // watch(source, cb)
      const newValue = runner()
      if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      runner()
    }
  }
  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)job.allowRecurse = !! cb } ...Copy the code

You can see that cleanup is assigned in the onInvalidate function and is a reference to the onStop cleanup function. Is called every time the side effect function executes, OnInvalidate is used to register an invalid callback that cleans up any side effects from the last side effect function. This invalid callback is executed every time the side effect function is re-executed or the current component is unloaded. The fn argument is a user-defined cleanup function. Next, a variable oldValue is declared to save the oldValue. Job function is a task function that performs side effects. Inside job function, watchEffect and watch can be distinguished by judging whether cb side effect function exists to perform their respective side effects.

function doWatch(
  source,cb,{ immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,instance = currentInstance
) {...let scheduler
if (flush === 'sync') {
  // It is directly equivalent to job, which is executed when the component is updated. It can be compared with Pre, and there is no judgment on the instance and the current state of the instance, so it will be executed synchronously when the component is updated
  scheduler = job
} else if (flush === 'post') {
  // After the component is updated, the job is pushed to a queue first instead of performing the side effect immediately
  scheduler = () = > queuePostRenderEffect(job, instance && instance.suspense)
} else {
  // default: 'pre'
  scheduler = () = > {
    // If the current component state is update complete, the job is pushed into a queuePreFlushCb queue, which is executed before the next update of the component
    if(! instance || instance.isMounted) { queuePreFlushCb(job) }else {
      // with 'pre' option, the first call must happen before
      // the component is mounted so it is called synchronously.
      // The setup() function is executing when the component is initialized.
      job()
    }
  }
}
...
}
Copy the code

Scheduler is a scheduler function that schedules execution jobs. Flush is a configuration property of options, which defaults to pre to rerunce the side effects function each time the component is updated. You can also manually specify the values sync and POST. Sync means that the side effects function is executed synchronously on each component update. Post executes side effects after component updates, so you can control the timing of side effects by specifying different attributes for the job. Flush applies to both Watch and watchEffect.

function doWatch(
  source,cb,{ immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,instance = currentInstance
) {...// Create a listener
        const runner = effect(getter, {
          lazy: true,
          onTrack,
          onTrigger,
          scheduler
      });
      // Collect listeners into the global properties of the current component (array), which I guess is related to stopping listeners when the component is uninstalled
      recordInstanceBoundEffect(runner);
      // initial run
      // If the cb side effect callback exists, it is watch. If options also sets immediate to true, it is required to execute the side effect immediately after the listener is initialized. Default is lazy execution.
      if (cb) {
        // Execute the side effect immediately
          if (immediate) {
              job();
          }
          // Lazy execution by default
          else{ oldValue = runner(); }}// Initialize the watchEffect listener after the component is updated
      else if (flush === 'post') {
          queuePostRenderEffect(runner, instance && instance.suspense);
      }
      // Execute a watchEffect immediately before the component is initialized, i.e. the listener function is initialized.
      else {
          runner();/ / execution effect
      }
      // Returns a function that stops listening after execution
      return () = > {
          stop(runner);
          if(instance) { remove(instance.effects, runner); }}; . }Copy the code

After the listener is created, the listener is returned. Runner receives the listener, so when runner is executed, the listener is triggered. At this point, it is either a dependency collection at initialization. Either a change in value causes a dependency update, and eventually the doWatch function returns a function that can be used to explicitly stop listening. Internally, it releases all dependencies in the listener by calling the stop method.

Ok, so here we have a brief analysis of the internal execution flow of the doWatch function. The inner part of doWatch is actually some initialization work around the listener, but we don’t seem to know exactly how the listener is created. So strike while the iron is hot, and look for the effect method in effect.ts, which creates an entrance to the listener function.

effect

// Determine if the fn passed in is already a listener, with the _isEffect attribute, if not, without the attribute identifier
export function isEffect(fn: any) :fn is ReactiveEffect {
  return fn && fn._isEffect === true
}

// Create the factory function effect for the listener
export function effect<T = any> (
  fn: () => T, // Wrap the original function of the source data
  options: ReactiveEffectOptions = EMPTY_OBJ {immediate, deep, flush, onTrack, onTrigger}}
) :ReactiveEffect<T> {
  // If the fn source function is already a listener, then it will attach a raw property to it, which is used to cache the original function body.
  // When passed in again, the internal source function is automatically retrieved and a new listener is created using the source function, so effect always returns a newly created listener.
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // Execute the created function
  const effect = createReactiveEffect(fn, options)
  // Lazy is a configuration property of the options option. If it is true, the side effect is executed lazily. Otherwise, the side effect is executed immediately after the listener is created
  // In vue component instances, the lazy attribute defaults to true (as seen in vue.global.js).
  if(! options.lazy) { effect() }// return the listener function
  return effect
}
Copy the code

If you go back to the effect.ts file and look at the effect function, it will first receive the fn source data function and the options configuration option, both of which we passed in manually. The isEffect method is then called to determine if fn is already a listener, and if so, its source data function (stored in the RAW property) is retrieved to perform the creation, so effect always returns a newly created listener. CreateReactiveEffect is called to perform the creation, and the listener is returned.

createReactiveEffect

let uid = 0 // the id identifier should be used to indicate uniqueness

// Execute create listener function
function createReactiveEffect<T = any> (
  fn: () => T, // Source data function
  options: ReactiveEffectOptions The configuration items can be {immediate, deep, flush, onTrack, onTrigger}.
) :ReactiveEffect<T> {
  // Initialize a listener function, which is also an object in nature, so you can mount/extend some useful properties
  const effect = function reactiveEffect() :unknown {
    // Active is a property extended on the effect listener. By default, active is true, indicating a valid listener, and goes when the value of the listener property changes
    // The only time active is false is when the stop method is fired, at which point the listener loses its ability to listen, i.e. responsiveness failure
    if(! effect.active) {// Scheduler is a custom scheduler that is used to schedule the trigger listener. If the listener fails, the scheduler will return undefined to terminate
      // If the program continues without a custom scheduler, the source data function is executed. Since the dependencies are removed, the dependency collection operation is not triggered
      // Just a function call
      return options.scheduler ? undefined : fn()
    }
    // The purpose of effectStack is to prevent the same listener function from firing multiple times in a row causing dead recursion.
    // If you are executing a side effect function, and the function has operations inside it to modify dependent properties, the modification will trigger, and so on
    // The listener function executes again, and the side function executes, so that the current side function recurses indefinitely, so to avoid this, the side function executes
    // the function is evaluated before execution. If the listener is not currently off the stack, nothing is executed.
    if(! effectStack.includes(effect)) {The cleanup function removes the effect listener function (also called the dependency function) in the targetMap and clears the effect listener deps
      You'll notice that the cleanup operation is performed before each side effect function is about to be executed, i.e., it cleans up previous dependencies before each dependency is recollected. The purpose of this is to ensure that
      // The dependency attribute time corresponds to the latest listener function.
      cleanup(effect)
      try {
        // The current effect listener is pushed and set to activeEffect is activated
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        //fn is a side effect function. If the reactive object in this function has access to properties, it will trigger the getter, which will call the track() method, and then implement the dependency collection
        return fn()
      } finally {
        // After the side effect function is executed, the current side effect function is removed from the stack and deactivated
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
  // Functions are also object types, so you can extend effect listeners with useful properties
  effect.id = uid++ //id Unique identifiereffect.allowRecurse = !! options.allowRecurse// Whether to allow recursion (this property is supposed to control whether the listener can be executed recursively, but it is not found, even if it is true)
  effect._isEffect = true // Is the listener identifier, which indicates that it is already a listener
  effect.active = true // Controls the responsiveness of the listener function, false will lose responsiveness
  effect.raw = fn // Cache the fn source data function
  effect.deps = [] // Storage depends on DEP.
  effect.options = options // Configurable options
  return effect// Returns the created listener function
}

This method is called every time a listener is about to execute a side effect function or when stop() is triggered
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

The createReactiveEffect method takes two parameters, both of which are passed from the effect function, so there is no explanation. The method initializes a reactiveEffect function internally, which is the listener function. Since functions are essentially object types, mount/extend some useful properties on them (attribute definitions can be seen in comments), and eventually create a good listener to return. A listener function is created. If it comes from vue.global.js, the returned effect will be received by runner and stored. (Listen to the internal execution of the function and we will go back to the analysis when firing below, so as to maintain a logical continuity).

// vue.global.js
 function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ, instance = currentInstance) {...// Runner saves the effect listener returned by the reactiveEffect method
         const runner = effect(getter, {
          lazy: true, onTrack, onTrigger, scheduler }); . }Copy the code

Depend on the collection

For the sake of illustration, suppose we now define a watchEffect and watch.

const count = ref(0)
const state = reactive({ count: 0 })
let dump, dump1

watchEffect(() = > {
  dump = count.value
})

count.value = 1

watch(
  () = > state.count,
  (count, prevCount) = > {
    dump1 = count
  }
)

state.count = 1
Copy the code

First, we define two responsive data count and state, and then we define two variables dump and dump1. We initialize the listener, which by default performs a dependency collection, and then modify the responsive data. This modification sends an update signal to the listener, which then executes the side effect function to implement the dependency update. It makes sense to look at the analysis, but if that’s the case, it’s better to go through the process and peek into the implementation details.

track

The track method is triggered when accessing reactive data to perform a dependency collection.

// Dependency collection function, which is triggered when a responsive data property is accessed to collect a listener (also called dependency function) effect on the accessed property
//target: original target object (the original object corresponding to the proxy object), type: operation type, key: access attribute name
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // If shouldTrack is false, or there is no active listener firing, then no dependencies are collected.
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  //targetMap is a WeakMap set, also called a dependency mapping table (container), which stores the original target object as the key and depsMap(which is a Map set) as the value
  //depsMap uses the access attribute as the key, and dep as the value. The DEP collection holds the effect listener function,
  // These listener functions can also be called dependency functions that access properties, which are triggered when the value of the access property changes.

  // Get dependency map
  let depsMap = targetMap.get(target)
  // If the dependency map does not exist, initialize one
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the dependency set corresponding to the access attribute in the dependency map
  let dep = depsMap.get(key)
  // If no dependency set exists, initialize one
  if(! dep) { depsMap.set(key, (dep =new Set()))}// Check whether the deP dependency set has a listener for the currently active state, if not, save it (this process is called dependency collection)
  if(! dep.has(activeEffect)) { dep.add(activeEffect)//activeEffect is actually the effect listener function for the active state currently being executed. This step pushes into the DEP set that stores all dependent functions related to the access properties
    // A bidirectional mapping is established in the deps (array) of the current listener function. This bidirectional mapping is used every time the cleanup operation is performed before the side effect function is executed
    // This listener (dependent function) will be removed from the deP set of all access properties previously collected in depsMap. Then re-execute the track function when executing the side effect function
    // Collect back, such operation seems to be a bit of pain, but after fine taste is really not pain, but in order to ensure the dependency of the latest.
    activeEffect.deps.push(dep)

    // Trigger the corresponding hook function only in the development environment (debug hook)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
Copy the code

The track method takes three parameters target: the original target object (the original object corresponding to the proxy object), type: the type of the operation, and key: the name of the access property. If shouldTrack is false, or if there is no active listener function firing, then dependencies are not collected (meaning there are no dependencies to collect). The following is a series of procedures for determining the existence of content stored in a dependent mapping table (container) and creating a stored procedure, resulting in a mapping table structure like this:

WeakMap<Target, Map<string | symbol, Set<ReactiveEffect>>> [[target1,[[key1,[effect1,effect2,...],[key2,[effect1,effect2,...],...]]],[target2,[[key1,[effect1,effect2,...],[key2,[e ffect1,effect2,...],...]]],...]Copy the code

It may not seem intuitive, so describe it: First, a dependency collection container of type WeakMap, targetMap, is created, which is a top-level container where all dependencies are arranged. Then in targetMap, the original target object of different targets will be used as key, and depsMap (Map set) will be used as value to divide the collection container into segments. DempsMap will divide the dempsMap interval again with the incoming access attribute named key and DEP (Set Set, with its own dep function) as value, and finally store the effect listener function (dependent function) currently Set as active state into THE DEP Set. So all the dependent functions for different access properties of different target objects are collected, and you can see how they are arranged. . Activeeffect.deps. Push (deP) also adds a deP to the dePS array for each effect. The purpose of this step is to establish a bidirectional mapping between each listener function and the dependency mapping table. This bidirectional mapping takes effect during the cleanup operation before each side effect function is executed. This listener (dependency function) is removed from the DEP set of all access properties previously collected in depsMap. Then, when the side effect function is executed, track is executed again to collect it back. Such operation seems to be a bit painful, but it is really not painful after careful examination. This is intentional in order to ensure the latest dependency.

Depend on the update

trigger

The trigger method is a bit too long.

// Trigger the dependency update function
export function trigger(
  target: object, // The original target object (the original object corresponding to the proxy object)
  type: TriggerOpTypes, // Operation typekey? : unknown,// The name of the property to be modified/setnewValue? : unknown,// New attribute valueoldValue? : unknown,// Old attribute valueoldTarget? :Map<unknown, unknown> | Set<unknown> // It is only used for debugging in development mode
) {
  // Get the dependency mapping set depsMap of the target object
  const depsMap = targetMap.get(target)
  // If depsMap does not exist, no dependency on the original object has been collected
  if(! depsMap) {// never been tracked
    return
  }

  // Initialize a set of effects to hold the listener functions to be executed.
  const effects = new Set<ReactiveEffect>()
  //add effect into effects
  //add is an addition method to add the effect listener to the Effects collection
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    //effectsToAdd is a set of effect listeners that execute attributes from depsMap
    if (effectsToAdd) {
      // Perform traversal add
      effectsToAdd.forEach(effect= > {
        // Add condition: The listener to be added must be in a deactivated state or have the allowRecurse configuration attribute true
        // However, the allowRecurse attribute does not recurse, neither true nor false listener effect
        // I feel this attribute is redundant.
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }// If the operation type is clear, all dependency functions related to the passed property are fired. Since the clean operation clears the entire collection, the dependency of each collection property is affected
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
    // Add the depsMap dependency on length and the dependency on index indices not greater than the length of the new array to effects
    // Because the array length is changed, all elements corresponding to the index of the array that is not larger than the new length will be reset once, thus triggering dependency updates
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    / / if you enter the else that must be SET | ADD | DELETE one of the operation, if the key is not for undefined, states that the key is a valid attribute, then get the attributes corresponding to all dependent function
    // Add to the Effects collection
    if(key ! = =void 0) {
      //void 0 = undefined
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    ITERATE_KEY and MAP_KEY_ITERATE_KEY dependencies are collected.
    In the basehandlers. ts -> ownKeys method, this method is triggered when listening for object.keys () to be called.
    //2. CollectionHandlers. Ts -> insert method size, iteration method ['keys', 'values', 'entries',forEach, Symbol. Get Set length. The size is triggered when the size method, called Map, Set the iteration method of collection (keys, values, entries, forEach, for... Of, etc.).
    //ADD indicates the operation of adding attributes, DELETE indicates the operation of deleting attributes, and SET indicates the operation of modifying attributes. Different operation types will trigger the update corresponding iteration dependencies according to the different types of target objects
    // These iteration dependencies need to be updated because the current operation has an impact on the collected dependencies. Without updating, the dependency data cannot be guaranteed to be up to date.
    switch (type) {
      // Add attribute operation
      case TriggerOpTypes.ADD:
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))}break
      // Delete attribute operation
      case TriggerOpTypes.DELETE:
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      // Modify attribute operation
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break}}// Update the execution method of dependent functions
  const run = (effect: ReactiveEffect) = > {
    // Debug hook functions in development mode
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // If the scheduler is mounted on the listener's options configuration, use the scheduler to execute the listener, otherwise execute the listener directly
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // Iterate over the Effects collection to listen for updates to the function.
  effects.forEach(run)
}
Copy the code

The trigger method internally obtains the depsMap dependency set under the current original target object from the dependency mapping table targetMap. If depsMap does not exist, it indicates that the attribute dependency of the original object has not been collected, and it directly returns without triggering the dependency update. And then as I go down, I’m going to initialize a set of effects to hold the listener functions that are going to be executed. Why do I use the set set, because the set is going to be unduplicated? Then set will filter it out automatically. We define an add function that adds the effect listener to the Effects collection. One detail inside the Add method is the condition under which the listener is added: The listener needs to be either inactive, or the allowRecurse configuration attribute is true. The “allowRecurse” attribute does not recurse either with the true or false listener effect.

  const effect = function reactiveEffect() :unknown {...if(! effectStack.includes(effect)) {... }... }Copy the code

Even if you add an effect equal to activeEffect to the effects set and then execute the listener, you are blocked by the if condition above, so I feel that allowRecurse is redundant. Two questions arise: when is effect equal to activeEffect? What if interception is judged without if? These two problems are not urgent, wait for the following analysis to effect function execution will be understood.

The following is a series of judgments about the type and key of the type operation passed in. The final aim is to add the corresponding DEP dependency set to the effects set. There is only one point that needs to be paid attention to:

// also run for iteration key on ADD | DELETE | Map.SET
ITERATE_KEY and MAP_KEY_ITERATE_KEY dependencies are collected.
In the basehandlers. ts -> ownKeys method, this method is triggered when listening for object.keys () to be called.
//2. CollectionHandlers. Ts -> insert method size, iteration method ['keys', 'values', 'entries',forEach, Symbol. Get Set length. The size is triggered when the size method, called Map, Set the iteration method of collection (keys, values, entries, forEach, for... Of, etc.).
//ADD indicates the operation of adding attributes, DELETE indicates the operation of deleting attributes, and SET indicates the operation of modifying attributes. Different operation types will trigger the update corresponding iteration dependencies according to the different types of target objects
// These iteration dependencies need to be updated because the current operation has an impact on the collected dependencies. Without updating, the dependency data cannot be guaranteed to be up to date.
switch (type) {
  // Add attribute operation
  case TriggerOpTypes.ADD:
    if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
        add(depsMap.get(MAP_KEY_ITERATE_KEY))
      }
    } else if (isIntegerKey(key)) {
      // new index added to array -> length changes
      add(depsMap.get('length'))}break
  // Delete attribute operation
  case TriggerOpTypes.DELETE:
    if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
        add(depsMap.get(MAP_KEY_ITERATE_KEY))
      }
    }
    break
  // Modify attribute operation
  case TriggerOpTypes.SET:
    if (isMap(target)) {
      add(depsMap.get(ITERATE_KEY))
    }
    break
}
Copy the code

By determining the three different types of type and then adding the iteration dependency to the Effects set and then updating it, why update the iteration dependency? When were these dependencies collected? Let’s start with the collection area.

  • baseHandlers.ts -> ownKeysIn the capture method, the method is triggered when it listensObject.keys()Is invoked.
  • CollectionHandlers. Ts -> Size, iterative method['keys', 'values', 'entries',forEach, Symbol.iterator]In the. Get the set length.sizeTriggered whensizeMethod, callMap,SetIterative methods of sets (keys,values,entries,forEach,for... of, etc.).

If you look at the judgment conditions for each of these types of cases, you’ll see that different types of operations have an effect on the dependencies under them. Let’s take an example;

const state = reactive({ a: 1.b: 2 })
let keys
watchEffect(() = > {
  keys = Object.keys(state)
})

state.c = 3
Copy the code

We define a reactive Object state and then execute Object.keys(state) in the watchEffect side effect function. First, the side effect function is executed once, and the ownKeys capture method is triggered during execution due to the Call to Object.keys. The ownKeys method will also trigger track, which will be used for dependent collection. This method is divided into two types: array and object. If array is used, length will be used as the key. We store their dependencies into the depsMap collection, which we call iterative dependencies.

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

Then we ADD a new property c with a value of 3. The new property will trigger the code, and the code must execute to the type we just mentioned, which is type ADD. Keys: ITERATE_KEY () : ITERATE_KEY () : ITERATE_KEY (); Otherwise, the keys stored in the keys variable are not up to date. Similarly, if you add elements to an array by pushing or setting an index, it will also cause dependency updates under length. And set types, we’re not going to do it all at once, but it’s the same thing. Oh, and I should add that the effects collection uses the Set type because it has the ability to de-duplicate elements, so it can avoid repetition.

The trigger method ends by defining an execution method run to update dependencies, and then iterates through the Effects collection to perform listener updates. This completes the trigger analysis. But we are not done with chapter analysis, and we will look at the implementation process.

Start iterating through effects, and then take the effect listeners that are stored in sequence and put them in the run method. You’ll see that the run method input receives effect. The first if judgment in the run method is irrelevant and is about debugging in development mode. If the scheduler is mounted on the listener’s options configuration, the listener is executed using the scheduler. Otherwise, the listener is executed directly. We have previously analyzed in vue. Global.js, which will mount a scheduler on options. Here is where it is used.

function doWatch(
  source,cb,{ immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,instance = currentInstance
) {...let scheduler
if (flush === 'sync') {
  // It is directly equivalent to job, which is executed when the component is updated. It can be compared with Pre, and there is no judgment on the instance and the current state of the instance, so it will be executed synchronously when the component is updated
  scheduler = job
} else if (flush === 'post') {
  // After the component is updated, the job is pushed to a queue first instead of performing the side effect immediately
  scheduler = () = > queuePostRenderEffect(job, instance && instance.suspense)
} else {
  // default: 'pre'
  scheduler = () = > {
    // If the current component state is update complete, the job is pushed into a queuePreFlushCb queue, which is executed before the next update of the component
    if(! instance || instance.isMounted) { queuePreFlushCb(job) }else {
      // with 'pre' option, the first call must happen before
      // the component is mounted so it is called synchronously.
      // The setup() function is executing when the component is initialized.
      job()
    }
  }
}
...
}
Copy the code

Bia the code out again to make a better impression. Let’s use the default mode (Pre) for analysis (it’s pretty much the same, just different timing). If the trigger is triggered after the component has been rendered, then the if is triggered. If the trigger is triggered before the component is mounted (such as setup), then the else is triggered. For simplicity, we will analyze the else. I’m going to call the job method again. We still bia job.

const job = () = > {
  // Runner is a listener function. If the active attribute is false, the listener is stopped and returns without performing any side effects
  if(! runner.active) {return
  }
  // Cb is the side effect function in the watch method. If it exists, execute it (side effect). If it does not, execute runner (effect itself)
  if (cb) {
    // watch(source, cb)
    const newValue = runner()
    if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
      // cleanup before running cb again
      if (cleanup) {
        cleanup()
      }
      callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [
        newValue,
        // pass undefined as the old value when it's changed for the first time
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onInvalidate
      ])
      oldValue = newValue
    }
  } else {
    // watchEffect
    runner() // effect() in 'effect.ts'}}Copy the code

There are two cases in a job, cb and no CB, which is the difference between a watch and a watch effect. If watch does the if, it calls runner (listener) to return the new property value returned by the original function (new dependencies are collected in the process), and the cleanup method is used to cleanup any side effects that are still in effect after the last side effect function was executed. Down to call callWithAsyncErrorHandling method, perform side effect function. Otherwise it’s watchEffect, else logic, watch, we didn’t analyze the internal execution of the effect listener function, we just wrote it here, don’t ask me why, just happy, ha, ha, ha, ha, let’s go back to the effect.

Find, find, find a friend, find a good friend…

// Initialize a listener function, which is also an object in nature, so you can mount/extend some useful properties
const effect = function reactiveEffect() :unknown {
  // Active is a property extended on the effect listener. By default, active is true, indicating a valid listener, and goes when the value of the listener property changes
  // The only time active is false is when the stop method is fired, at which point the listener loses its ability to listen, i.e. responsiveness failure
  if(! effect.active) {// Scheduler is a custom scheduler that is used to schedule the trigger listener. If the listener fails, the scheduler will return undefined to terminate
    // If the program continues without a custom scheduler, the source data function is executed. Since the dependencies are removed, the dependency collection operation is not triggered
    // Just a function call
    return options.scheduler ? undefined : fn()
  }
  // The purpose of effectStack is to prevent the same listener function from firing multiple times in a row causing dead recursion.
  // If the side effect function is being executed and there is an operation inside the function to modify the dependent properties, the modification will trigger the trigger, which will trigger the listener again.
  // Then the side effect function executes, so that the current side effect function recurses indefinitely, so to avoid this phenomenon, the first judgment is made before the side effect function executes.
  // If the listener is not already on the stack, nothing is executed.
  if(! effectStack.includes(effect)) {The cleanup function removes the effect listener function (also called the dependency function) in the targetMap and clears the effect listener deps
    You'll notice that the cleanup operation is performed before each side effect function is about to be executed, i.e., it cleans up previous dependencies before each dependency is recollected. The purpose of this is to ensure that
    // The dependency attribute time corresponds to the latest listener function.
    cleanup(effect)
    try {
      // The current effect listener is pushed and set to activeEffect is activated
      enableTracking()
      effectStack.push(effect)
      activeEffect = effect
      //fn is a side effect function. If the reactive object in this function has access to properties, it will trigger the getter, which will call the track() method, and then implement the dependency collection
      return fn()
    } finally {
      // After the side effect function is executed, the current side effect function is removed from the stack and deactivated
      effectStack.pop()
      resetTracking()
      activeEffect = effectStack[effectStack.length - 1]}}}Copy the code

The first step is to judge the response status of the listener function currently executed. Only responsive listener functions can be scheduled to execute. So when does it become unresponsive? This is when the stop (to explicitly stop listening) method is called, or when the component is unloaded. The second step is to determine whether an effect is already in the effectStack. This step is to prevent the same listener function from firing multiple times in a row causing dead recursion. Remember the two questions we had up there when I asked them? When is effect equal to activeEffect? Determine the result of interception without if. So now let’s analyze it. I’ll give you an example.

it('could control implicit infinite recursive loops with itself when options.allowRecurse is true'.() = > {
  const counter = reactive({ num: 0 })

  const counterSpy = jest.fn(() = > counter.num++)
  effect(counterSpy, { allowRecurse: true })
  expect(counter.num).toBe(1)})Copy the code

This single measure chestnut is used to test will! Effectstack.includes (effect) determine what happens if we change the if to true, and then run the single test instance to find a RangeError: Maximum Call stack size exceeded (Stack overflow). This is what happens if you don’t use the if judgment. How does that happen? Let’s look at single test instance execution. Effect is initialized by defining a reactive object counter, and the listener is initialized once. If true, run cleanup before fn. The cleanup method lets look inside:

This method is called every time a listener is about to execute a side effect function or when stop() is triggered
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

The effect listener is mounted with a DEPS property that holds all the DEPS containing the listener. This creates a bidirectional mapping between the listener and the dependent collection. So, after walking through the DEPS and removing the effect in each DEP, The listener in the dependency set is removed. Thus achieving the purpose of removing dependencies. After removing the dependencies, push the effect listener into the effectStack, called pushing, and set it to active activeEffect, then execute the side effect function. () => counter. Num ++, counter. Num => counter. The purpose of removing all previous dependencies before each side effect is to keep them up to date. The next increment operation is a modification operation, thus triggering the setter to trigger the dependent update. The activeEffect is still the same as the effect before the side effect function is executed. When the side effect function is executed again, it will return to our if judgment. Effectstack.includes (effect) determines the condition, and the process continues, thus falling into the trap of listening for implicit recursion inside the function. Therefore, this if judgment is crucial to avoid listening for implicit recursion inside the function. I think the answer to those two questions is pretty obvious.

With the single test example, the internal execution process of the side effect function was also analyzed, and the content of the Effect chapter was completely analyzed. At this time, I wrote many words in one sentence, ha ha ha