This is the second day of my participation in the wenwen Challenge

Effect is similar to the watch in vue2 source code, observer/subscriber. In the following procedure, effect is the observer, Target’s property value is the observed, effect looks at target’s property value, and target’s property value is changed to notify Effect. Be sure to view the source code in this way.

Side effect

Side effects are a relatively obscure concept, originally derived from “pure functions,” an early concept in programming, as you can see here and here. Features of pure functions:

  • It should always return the same value. No matter how many times the function is called, whether it is called today, tomorrow, or sometime in the future.
  • Self-contained (without using global variables).
  • It should not modify the state of the program or cause side effects (modifying global variables).

So, the side effects of pure functions are:

A function that does something else besides returning a certain value is called a “side effect.”

Console.log (123), for example, has a side effect. It returns undefined as the main function, but we don’t need its main function, its side effect is to print 123 on the console, we need its side effect.

Side effects in Vue 3 So let’s start with “main effects.” In the Vue world, the view layer and the DOM layer are two different things, although some people think they are. The main function of change-responsive data is that the changed data can be rendered to the view layer. But basically that’s the most important thing about the front end, and the side effects are the other ripple effects of reactive data changes, and the subsequent logic, and these ripple effects are called side effects. In medicine, side effects tend to be adverse reactions, but in Vue3 they are not. The title “remove side effects” does not mean that side effects should be removed because they are undesirable, but Vue3 provides a way to remove side effects at any time.

The main side effects are: DOM update, watchEffect, Watch, computed…

Effect and Target mapping

// Store the effect Set
type Dep = Set<ReactiveEffect>
// Any indicates the attribute of the monitored object, which corresponds to the Dep
// Stores all properties and dePs of monitored objects
type KeyToDepMap = Map<any, Dep>
/ / any target
const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code

The purpose of this code is to use Proxy monitoring as a target, and every time a handler is triggered it is an attribute dimension, so you need to maintain a mapping of attributes to the Effect collection.

Dep == “effect collection

KeyToDepMap === all attributes of target correspond to Dep

TargetMap === All attributes of target

To modify the corresponding attribute of target, the effect corresponding to the attribute can be found through the above mapping to trigger execution.

The type of effect

export interface ReactiveEffect<T = any> {
  (): T
  // effect flag property
  _isEffect: true
  // Unique identifier
  id: number
  active: boolean
  // The original function
  raw: () = > T
  // A collection of observers associated with the effect's response data
  // Effect is the same object as the collection of observers to which the corresponding response data points
  // The purpose is to remove the effect directly from the DEPS when it needs to be separated from the response data, and also remove the effect from the corresponding observer set of the response data, since it points to the same data
  deps: Array<Dep>
  // effect configuration options
  options: ReactiveEffectOptions
}
The active property, when true, is aware of the effect dependency and executes the onStop event
// Stop is triggered inside unmountComponent
// So stop is triggered when the component dies, and if active is true it clears the dependency trigger onStrop
stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false}}Copy the code

As you can see from the effect creation function below, when active is false, effect is a normal function passed in or undefined, with no listening dependencies

Effect configuration property

export interface ReactiveEffectOptions {
  // Whether to execute immediatelylazy? : boolean// Scheduler, whether to customize scheduling effectscheduler? :(job: ReactiveEffect) = > void
  // Whether to collect the listener functiononTrack? :(event: DebuggerEvent) = > void
  // Whether to trigger the listener functiononTrigger? :(event: DebuggerEvent) = > void
  // Whether to stop the listener functiononStop? :() = > void
}
// Event type
export type DebuggerEvent = {
  // effect
  effect: ReactiveEffect
  // Listen to the object
  target: object
  // Type, which depends on the type, such as ADD, DELETE, CLEAR
  type: TrackOpTypes | TriggerOpTypes
  // Porxy listener properties
  key: any
} & DebuggerEventExtraInfo
exportinterface DebuggerEventExtraInfo { newValue? : any oldValue? : any oldTarget? :Map<any, any> | Set<any>
}
Copy the code

Effect constructor

export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  // If fn is a listener, get the original function and recreate it
  // Effect returns a new monitor function each time
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // The core is createReactiveEffect
  const effect = createReactiveEffect(fn, options)
  The lazy attribute identifies whether it is executed once by default
  if(! options.lazy) { effect() }return effect
}
Copy the code

Pass in a function that returns an effect constructor

3. CreateReactiveEffect function

function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
  // Create a function of type ReactiveEffect
  const effect = function reactiveEffect() :unknown {
    // If active is false, dependency collection will not take place
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    // effectStack refers to the effect stack
    // If there is no effect in the effectStack, execute
	// To prevent cyclic dependencies
	// If effect1 triggers effect2, effect2 triggers effect2, effect2 triggers effect1, but effect1 is still in the effectStack, it will not enter
    if(! effectStack.includes(effect)) {// Clear the monitoring data in effect
      // And clear the effect observer from the monitoring data dependency
      // Clear bidirectionally, mainly through the deps attribute
      cleanup(effect)
      try {
        // Push a shouldTrack to the trackStack
        ShouldTrack is mainly whether to rely on collecting flag bits
        enableTracking()
        // Push an effect onto the effectStack
        effectStack.push(effect)
        // Active effect points to this effect
        activeEffect = effect
        // Execute fn to fire the getter corresponding to the internal monitor data to collect the effect dependency
        return fn()
      } finally {
        // Pop this effect
        effectStack.pop()
        Popup shouldTrack / /
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
  // Set the base properties of effect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
// record the last shouldTrack state
// and now set to false
// This function is executed when wraning, for example
export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}
// record the last shouldTrack state
// And now dependencies can be collected because fn, fetch is performed
export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}
// effect completes, then shoudTrack is set to its previous state
export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}
Copy the code

The original function fn is redefined as effect, and its main function is similar to watch as an observer

The cleanup function

Effect: To clear the effect dependency

function cleanup(effect: ReactiveEffect) {
  // Get the observed effect set associated with this effect
  // The deP associated with this effect points to the same storage address
  const { deps } = effect
  if (deps.length) {
    // Remove this effect from the deP
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    // And remove each term of deps
    deps.length = 0}}Copy the code

Deps [I]. Delete removes the connection between observed and effect, and DEps. Length =0 removes the connection between effect and observed.

Track function

What it does: Depends on collection

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // Return if no dependent collection is performed or if the target observer is undefined
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  // Gets the attributes of the target and the observer collection box set
  DepsMap 
      
       , key is tarfet
      ,>
  let depsMap = targetMap.get(target)
  // If it does not exist, recreate it
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the dependency set corresponding to the key
  let dep = depsMap.get(key)
  // If it does not exist, recreate it
  if(! dep) { depsMap.set(key, (dep =new Set()))}// Check whether the effect observer exists
  if(! dep.has(activeEffect)) {// Add this effect to the deP of the corresponding attribute value
    dep.add(activeEffect)
    // The deps in this effect also adds the DEP of the observed attribute value
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      // Trigger the onTrack function
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
Copy the code

It mainly collects the target targetEffect, and the DEPS of this targetEffect also adds the DEP corresponding to the observed attribute value to do double mapping

Trigget function

Function: notifies the dependency to execute the callback function

export function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  // Get all observers associated with target
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }
  // Add a dependency Set variable to ensure uniqueness
  const effects = new Set<ReactiveEffect>()
  // Add function by name
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        // Avoid circular dependencies
        if(effect ! == activeEffect) { effects.add(effect) } }) } }// Clear refers to target clearing all events triggered by attributes
  // Target = null
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    // Iterate over the observation set of all properties and add it to Effects
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // Monitor array length changes. Pop, shift, push, unshift, etc
    depsMap.forEach((dep, key) = > {
	  // Add all attributes with length and key greater than or equal to length to effects
	  // Keys smaller than length are collected automatically
	  Array.length (); array. length ()
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
	// Add activeEffect for the deP of the corresponding attribute
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }
	// The ITERATE_KEY collection function is the fifth parameter of the proxy handler, which monitors the collection of Object attributes, such as object. keys, for(key in target), etc
	// function ownKeys(target: object): (string | number | symbol)[] {
  	// track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  	// return Reflect.ownKeys(target)
	// }
    // also run for iteration key on ADD | DELETE | Map.SET
	// Delete add to change object and array keys length and length
	// For example, when using watch to monitor target deeply, it will perform recursive monitoring of key in target, so ownKeys will be triggered. When target deletes and adds attributes, it will also monitor
	// May have other effects, to be continued after use
    constisAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && ! isArray(target))if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  const run = (effect: ReactiveEffect) = > {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
	// If there is a scheduler, the scheduler is run
	// Otherwise, effect is executed
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}
Copy the code

conclusion

Effect is equivalent to watch. There is a mapping table between the properties of target and effect. The acquisition of the property value of KeyToDepMap Target will trigger Track, and the modification of the property value of activeEffect Target will trigger trigger. At this point, the KeyToDepMap associated with the object is retrieved from targetMap using target, and the DEP is retrieved from KeyToDepMap using the attribute key. The deP is added to the execution queue by traversing the DEP