Bidirectional Binding Lite (version 3.2) — Track Trigger Effect

The core functions of bidirectional binding in Vue3 are:

  • track
  • trigger
  • effect

There is also targetMap, which is a dependency cache of type WeakMap.

const targetMap = {
    target: {
        key: new Set([effect]),
    },
};
Copy the code

Objects are used here to represent general data structures.

TargetMap: Key is the target object, and value is WeakMap of depsMap

DepsMap is the key of the target object, and value is the DEP Map

Dep is a Set data structure with a value of effect.

We can implement a reactive expression with these three functions

setup() {
      const state = {
        msg: 'Hello World'.showMsg: true
      }

      effect(() = > {
        console.log(state.msg) // Hello Vue3
        track(state, 'get'.'msg')})setTimeout(() = > {
        debugger
        state.msg = 'Hello Vue3'

        trigger(state, 'set'.'msg'.'Hello Vue')},1000)

      return{}}Copy the code

The track function takes three arguments, the target object, the track operation type, and the object key.

  • Target: The object to be traced
  • Type: type of operation (get | has | iterate)
  • Key: The key corresponding to the object to be traced

The trigger function accepts six parameters, the target object, the trigger operation type, the object key, the new value, and the old value, for debugging

  • Target: The object to be traced
  • Type: the type of operation (the set | add | delete | clear)
  • Key: The key corresponding to the object to be traced
  • NewValue: the newValue of the set operation
  • OldValue: indicates the oldValue before the operation
  • OldTarget: used for debugging

The effect function is used to define the side effect function. The configuration and targetMap define a data structure.

/ / pseudo code
targetMap: {
    target: depsMap
}

depsMap: {
    key: dep
}

dep: [effect1, effect2, ...]

// targetMap: Type WeakMap
// depsMap: The type is Map
// dep: the type is Set

Copy the code

Therefore, in a responsive process, the effect function defines the side effect function, the track function tracks dependencies, and the trigger function triggers the side effect function.

Therefore, in Vue3’s response formula, the effect function is actually executed first.

export function effect<T = any> (fn: () => T, options? : ReactiveEffectOptions) :ReactiveEffectRunner {
    if ((fn as ReactiveEffectRunner).effect) {
        fn = (fn as ReactiveEffectRunner).effect.fn;
    }

    const _effect = new ReactiveEffect(fn);
    if (options) {
        extend(_effect, options);
        if (options.scope) recordEffectScope(_effect, options.scope);
    }
    if(! options || ! options.lazy) { _effect.run(); }const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
    runner.effect = _effect;
    return runner;
}
Copy the code

The effect function takes two parameters, a side effect function and a configuration object.

The process for merging configuration options is as follows

  1. Create a ReactiveEffect instance
  2. Execute the instance run function
  3. Define runner and assign the instance to the runner. Effect property
  4. Return to the runner

When are dependencies collected?

Let’s look at reactiveeffects.

export class ReactiveEffect<T = any> {
    active = true;
    deps: Dep[] = [];

    // can be attached after creationcomputed? :boolean; allowRecurse? :boolean; onStop? :() = > void;

    constructor(
        public fn: () => T,
        public scheduler: EffectScheduler | null = null, scope? : EffectScope |null
    ) {
        recordEffectScope(this, scope); New scope API in version 3.2
    }

    run() {
        if (!this.active) {
            return this.fn();
        }
        if(! effectStack.includes(this)) {
            try {
                effectStack.push((activeEffect = this));
                enableTracking();

                trackOpBit = 1 << ++effectTrackDepth;

                if (effectTrackDepth <= maxMarkerBits) {
                    initDepMarkers(this);
                } else {
                    cleanupEffect(this);
                }
                return this.fn();
            } finally {
                if (effectTrackDepth <= maxMarkerBits) {
                    finalizeDepMarkers(this);
                }

                trackOpBit = 1 << --effectTrackDepth;

                resetTracking();
                effectStack.pop();
                const n = effectStack.length;
                activeEffect = n > 0 ? effectStack[n - 1] : undefined; }}}stop() {
        if (this.active) {
            cleanupEffect(this);
            if (this.onStop) {
                this.onStop();
            }
            this.active = false; }}}Copy the code

As you can see, in the constructor constructor, only fn and scheduler are bound to this.

Let’s look at the run function.

This. active Determines whether it is valid and whether to execute stop. Then determine if the current instance already exists in the side effect function stack. The stack here is designed to handle nested calls to the effect function.

ActiveEffect assigns the current this value to the global variable while pushing the stack.

function e1() {
    console.log('e1');
}

function e2() {
    effect(e1);
    console.log('e2');
}

effect(e2);
Copy the code

Because the execution of a function is itself a pushout operation, a stack of side effects functions is used to ensure that the activeEffect instance is correct.

And then you have trackOpBit and effectTrackDepth. The two variables are bitwise shifted to the left to indicate the hierarchy of nested calls to the effect function.

First look at the implementation of the track function

export function track(target: object.type: TrackOpTypes, key: unknown) {
    if(! isTracking()) {return;
    }
    let depsMap = targetMap.get(target);
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
    }
    let dep = depsMap.get(key);
    if(! dep) { depsMap.set(key, (dep = createDep())); }const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined;

    trackEffects(dep, eventInfo);
}
Copy the code

If we omit the judgment statement, we can see that the main logic of the track function is to get the DEP from targetMap and create a new one if it doesn’t exist. You end up with a targetMap structure. A new DEP is created using createDep.

export constcreateDep = (effects? : ReactiveEffect[]):Dep= > {
    const dep = new Set<ReactiveEffect>(effects) as Dep;
    dep.w = 0; // Old dependency, already traced
    dep.n = 0; / / new dependencies
    return dep;
};
Copy the code

The main purpose of this function is to define two attributes, w and n. Indicates whether a dependency is traced.

The trackEffects function is then executed.

export function trackEffects(dep: Dep, debuggerEventExtraInfo? : DebuggerEventExtraInfo) {
    let shouldTrack = false;
    if (effectTrackDepth <= maxMarkerBits) {
        if(! newTracked(dep)) { dep.n |= trackOpBit;// set newly tracked
            shouldTrack = !wasTracked(dep);
        }
    } else {
        // Full cleanup mode.shouldTrack = ! dep.has(activeEffect!) ; }if(shouldTrack) { dep.add(activeEffect!) ; activeEffect! .deps.push(dep); }}Copy the code

If the effect recursion level is smaller than the maximum level, it determines whether it is a new dependency. The logic to determine whether it is a new dependency is defined by dep.n & trackOpBit > 0. Since a new DEP is created the first time a dependency is collected, the n attribute of the new DEP is 0. This makes trackOpBit 2. So dep.n & trackOpBit equals zero; Returns false. Then set the dep. N for dep. N | trackOpBit. The next step is to determine whether the DEP has been collected, and if so, not again.

There is also a case where the trace level exceeds the maximum level, at which point the logic of clearing all dependencies is removed.

Finally, collect the dependencies. The end of the track. Enter the finally logic.

Finally logic mainly does a state traceback.

This includes deP, trackOpBit, effectTrackDepth, effectStack, and so on.

This is where the dependency collection is done. Next comes the triggering dependency.

Trigger dependencies use the trigger function.

export function trigger(
    target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
    const depsMap = targetMap.get(target);
    let deps: (Dep | undefined=) [] []; deps.push(depsMap.get(key));if (deps.length === 1) {
        if (deps[0]) {
            triggerEffects(deps[0]); }}else {
        const effects: ReactiveEffect[] = [];
        for (const dep of deps) {
            if (dep) {
                effects.push(...dep);
            }
        }
        triggerEffects(createDep(effects));
    }
}
export function triggerEffects(dep: Dep | ReactiveEffect[], debuggerEventExtraInfo? : DebuggerEventExtraInfo) {
    // spread into array for stabilization
    for (const effect of isArray(dep) ? dep : [...dep]) {
        if(effect ! == activeEffect || effect.allowRecurse) {if (effect.scheduler) {
                effect.scheduler();
            } else{ effect.run(); }}}}Copy the code

Trigger code flow is actually quite clear, that is, go to targetMap to get the corresponding DEP, and then execute all effects. If there is a scheduler, execute scheduler, otherwise execute run.

This is a full reactive flow.

effect -> _effect.run() -> try {} -> track -> finally {} -> trigger

The end of the

The Vue version of this article is 3.2. It is for personal understanding only. Any errors or omissions are welcome to point out.