SoFunction
Updated on 2025-03-08

Vue3 series effect and ReactiveEffect track trigger source code analysis

introduction

Introduce a fewAPIWhen we found that it often appears in iteffecttrackandtrigger, although I'll briefly saytrackfor dependency collection,triggerto trigger updates. But after all, I didn't see the specific implementation and I felt unsure. Now we can find out.

1. ReactiveEffect

1. Related global variables

As mentioned earliereffect, that'sReactiveEffectExamples of . Some important global variables were used.

  • targetMap: Weak map to target objecttargetforkey, the dependency set it collectsdepsMapis a value, so through the target objecttargetAll corresponding dependencies can be obtained;
  • activeEffect: Currently activeeffect, and will be collected later;
  • shouldTrack: Used as a flag to pause and resume dependency collection;
  • trackStack:historyshouldTrackrecord stack.

targetMapcontrastreactiveThe chapter mentionedproxyMap

  • Both are weak mappings;
  • All target objectstargetforkey
  • targetMapThere is only one overall situation;proxyMapThere are four types, corresponding toreactiveshallowReactivereadonlyshallowReadonly
  • onetargetIn aproxyMapThere is at most one corresponding agentproxy,thereforeproxyMapThe value of a singleproxyobject;
  • onetargetIt can be depended on by manydep,thereforetargetMapThe value of the datasetMap
const targetMap = new WeakMap<any, KeyToDepMap>()
export let activeEffect: ReactiveEffect | undefined
export let shouldTrack = true
const trackStack: boolean[] = []

And functions that control pause and restore dependency collection:

// Pause collectionexport function pauseTracking() {
  (shouldTrack)
  shouldTrack = false
}
// Recovery collectionexport function enableTracking() {
  (shouldTrack)
  shouldTrack = true
}
// Reset to the previous stateexport function resetTracking() {
  const last = ()
  shouldTrack = last === undefined ? true : last
}

2. class statement

Initialize in the constructorfn( implementrun()call ) , schedulerscheduler, and passrecordEffectScopeto record the scope of an instance; declare some instance properties, andrunstopTwo methods:

  • activebooleanType, indicating the currenteffectWhether it works;
  • deps:currenteffectdependency;
  • parent: Point to the previous activeeffect, form a linked list;
  • computed: Optional, incomputedThe function is obtainedComputedRefImplInsideeffectHave this attribute;
  • allowRecurse, optional, indicating whether self-calling is allowed;
  • deferStop: Private, optional, indicatingstop()Whether to delay execution;
  • onStop: optional, function, in executionstop()Will be called whenonStop
  • onTrack
  • onTrigger: These twolistenerFor debugging purposes, it is fired when dependency collection and responsive updates are respectively;
  • runeffectThe most core method.
  • stop: CalledcleanupEffectleteffectStop working, if it isstopCurrently activeeffect, that is, stop yourself, and you willdeferStopAdjust totrue, thereby delaying the stop time; triggeringonStop;WillactiveAdjust tofalse
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean
  /**
   * @internal
   */
  private deferStop?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }
  run() {
    if (!) {
      return ()
    }
    // Currently active effect    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    // If the currently active effect is this effect itself, then return it directly    while (parent) {
      if (parent === this) {
        return
      }
      parent = 
    }
    // Active effects are successively formed into a linked list, connected by the parent attribute    try {
       = activeEffect
      activeEffect = this
      shouldTrack = true
      trackOpBit = 1 << ++effectTrackDepth
      if (effectTrackDepth <= maxMarkerBits) {
        // Traversal Set the effect in it to captured tracked        initDepMarkers(this)
      } else {
        // Level overflow clears the current side effects        cleanupEffect(this)
      }
      // The incoming fn is called      return ()
    } finally {
      // Because there is a return in front, it is executed when an exception occurs in the code block of try      if (effectTrackDepth <= maxMarkerBits) {
        // This method traverses , deletes the outdated effect and adds the uncaught effect        // effect is the dep        finalizeDepMarkers(this)
      }
      trackOpBit = 1 << --effectTrackDepth
      // Restore some states      activeEffect = 
      shouldTrack = lastShouldTrack
       = undefined
      // If delay stop is set, execute stop and perform delay cleanup      if () {
        ()
      }
    }
  }
  // Clear side effects  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
       = true
    } else if () {
      cleanupEffect(this)
      if () {
        ()
      }
       = false
    }
  }
}

3. cleanupEffect

cleanupEffectUsed to remove side effects. Receive oneeffect, traversaland remove side effects one by oneeffect. Then clear

function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if () {
    for (let i = 0; i < ; i++) {
      deps[i].delete(effect)
    }
     = 0
  }
}

2. Effect function

1. Related ts types

effectThere are several related types of functions:

  • ReactiveEffectOptionseffectOne of the entry parameters types of the function;
  • ReactiveEffectRunner: is a function and haseffecttype of attribute;
export interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}
export interface ReactiveEffectRunner<T = any> {
  (): T
  effect: ReactiveEffect
}

2. Function declaration

effectThe function has two parameters:

  • fn: is a function that is used to createReactiveEffectExample_effect
  • options: Optional, for coverage_effectattributes on.
export function effect&lt;T = any&gt;(
  fn: () =&gt; T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // Process fn  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).
  }
  // Create a _effect based on fn  const _effect = new ReactiveEffect(fn)
  if (options) {
    // Use options to override the properties on _effect    extend(_effect, options)
    if () recordEffectScope(_effect, )
  }
  // Without lazy , then _effect executes run() immediately  if (!options || !) {
    _effect.run()
  }
  // runner: Get _effect.run and hang the effect attribute, wrap it into ReactiveEffectRunner type  const runner = _effect.(_effect) as ReactiveEffectRunner
  // The effect attribute refers to the _effect itself, which is convenient for using runner to call run and stop   = _effect
  // Return to runner  return runner
}

3. stop function

stopUsed for clearingeffect. Enter the ginseng asReactiveEffectRunner

export function stop(runner: ReactiveEffectRunner) {
  ()
}

3. Track dependency collection

1. track

Always talkingtrackConduct dependency collection, here we see how it is done.

  • To target objecttargetforkeydepsMapfortargetMapvalue;targetofkeyforkey,usecreateDep()Create dependenciesdepas value, stored intargetCorrespondingdepsMapmiddle.
  • passtrackEffects(dep, eventInfo)To collect side effects.
// Global variable targetMapconst targetMap = new WeakMap&lt;any, KeyToDepMap&gt;()
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack &amp;&amp; activeEffect) {
    let depsMap = (target)
    if (!depsMap) {
      (target, (depsMap = new Map()))
    }
    let dep = (key)
    if (!dep) {
      (key, (dep = createDep()))
    }
    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined
    trackEffects(dep, eventInfo)
  }
}

2. createDep

usecreateDepCreate a newdep. You can see,depIt's aSetInstance, and two attributes are added:

  • wwasTrackedThe first letter of the current dependency is collected;
  • nnewlyTrackedThe first letter of , indicates whether the current dependency is newly collected.
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
   = 0
   = 0
  return dep
}

3. trackEffects

trackEffectsUsed to collect side effects. Mainly put the currently activeactiveEffectjoin indep, and inAdd all dependencies affected by this side effect.

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth &lt;= maxMarkerBits) {
    if (!newTracked(dep)) {
       |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !(activeEffect!)
  }
  // The current dependency dep has not been captured yet / When the current dependency dep is in the current dependency dep and there are no currently active side effects,  // Add the currently active side effects effect to dep, and add dep to the dependency set affected by side effects  if (shouldTrack) {
    (activeEffect!)
    activeEffect!.(dep)
    if (__DEV__ &amp;&amp; activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!
      })
    }
  }
}

4. Summary

In a difficult sentence, relying on collection is to take the side effects of current activenessactiveEffectSave global variablestargetMapIn (targetCorrespondingdepsMap) middle (targetofkey) correspondingdep(Type isSet) and put thisdepJoin to the recipientactiveEffectAll dependencies affected by side effectsin the list.

4. trigger

Triggering updates is actually triggering side effects, so this subsection decides totrackIn reverse order.

1. triggerEffect

triggerEffectTrigger side effects to update. Side effects of triggering updateeffectWhen self-calling is allowed, and is not currently active side effects, through the schedulerschedulerExecute side effects or directlyrun, is where updates are actually triggered.

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || ) {
    if (__DEV__ &amp;&amp; ) {
      (extend({ effect }, debuggerEventExtraInfo))
    }
    // Where to actually trigger updates    if () {
      ()
    } else {
      ()
    }
  }
}

2. triggerEffects

Receive onedepand additional information for debugging. TraversaldepIn-houseeffect, use one by onetriggerEffectto perform side effects. The source code is a bit of honey here.

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  // Are the two mutually exclusive, but the operations performed the same?  And why not write it in a for...of...?  for (const effect of effects) {
    if () {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

3. trigger

Always said beforetriggerTrigger update, it is actually known now, but it is actuallytriggerEffectto perform side effects to achieve updates.

Here is to create adepsArray, according totargetkeyand the operation type that triggers updatestypeetc. to get all related parametersdep, put indeps. Take it out againdepsAll indepAll ineffect, put ineffectsIn the list, bytriggerEffects(effects)to trigger all related side effects and finally achieve updates.

It should be noted that for arrays:

  • ReviselengthProperties will cause updates to all dependencies of the array;
  • Revising the array will causelengthUpdates of attribute-related dependencies becauselengthThe value of .
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map&lt;unknown, unknown&gt; | Set&lt;unknown&gt;
) {
  const depsMap = (target)
  if (!depsMap) {
    // never been tracked
    return
  }
  // Used to gather all related dependencies  let deps: (Dep | undefined)[] = []
  if (type === ) {
    // Call the clear method of Set and Map instances, which will trigger all related side effects    // collection being cleared
    // trigger all effects for target
    deps = [...()]
  } else if (key === 'length' &amp;&amp; isArray(target)) {
    // When the target object is an array, and the length attribute is modified, all related side effects will be triggered    ((dep, key) =&gt; {
      if (key === 'length' || key &gt;= (newValue as number)) {
        (dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      ((key))
    }
    // also run for iteration key on ADD | DELETE | 
    switch (type) {
      case :
        if (!isArray(target)) {
          ((ITERATE_KEY))
          if (isMap(target)) {
            ((MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // Changes to array subscript members will cause updates related to the length attribute          // new index added to array -&gt; length changes
          (('length'))
        }
        break
      case :
        if (!isArray(target)) {
          ((ITERATE_KEY))
          if (isMap(target)) {
            ((MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case :
        if (isMap(target)) {
          ((ITERATE_KEY))
        }
        break
    }
  }
  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined
  if ( === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        (...dep)
      }
    }
    // Here triggerEffects accepts parameter type Set, before it is an array    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

5. Summary

1. Dependency collection

targetMapThere isdepsMap(bytargetforkey);depsMapThere are many indep(bytargetMapofkeyforkey); simply understood as: when compiling, according totargetandkey, create side effects, willactiveEffectPoint to newly created side effects and store them in related dependenciesdepThe process in it is relying on collection.

2. Trigger update

In turn, triggertargetkeyRelateddepAll related side effects are passed through the()or()to implement updates.

The above is the detailed content of the source code analysis of the Vue3 series effect and ReactiveEffect track trigger. For more information about Vue3 effect and ReactiveEffect track trigger, please follow my other related articles!