Let’s start with two global constants and one variable.

  • TargetMap [new WeakMap] : When track(), trigger(), hit the intermediate correlation container
  • EffectStack [Array] : As the name suggests, is the container that stores effects
  • ActiveEffect: Can be used as a temporary variable when track() is used as a collection object for Target

1, reactive (target)

Reactive is implemented by reactive (BB)

  • Basically, new Proxy() overrides getter and setter methods to implement the logic you need
  • Overwrite getter to implement track collection effect
  • Override setter reality trigger trigger effect

2, the effect (fn, options)

Effect is the most important part of the track() process and is the only place where activeEffect is provided

3, the computed (getterOrOptions)

The above flow chart shows that the essence of computed is effect.

  • 1. Get an anonymous function getter by processing the getterOrOptions parameter
  • 2. Take the getter as the effect() argument to get a delayed runner function
  • 3. Wrap runner into a ref object and return it.

4, watch (source, cb, options)

Compared to computed, the implementation logic of Watch is relatively complex. This is just a rough implementation. Through the analysis of the source code, it is found that the general principle of Watch is also based on the implementation of Effect.

  • Const getter = ()=> source; const getter = ()=> source;
  • 2. Get a delayed runner function by effect
  • 3. First run runner to complete track()
  • 4. Return an anonymous function to stop listening

5. Simple implementation

Key note: the following code (omitted a lot of logic) for the principle of understanding, specific implementation please move to VUE-next source code.

5.1, reactive

Based on the analysis and understanding of reactive(Target), the basic implementation of Reactive is as follows:

function reactive(obj) {
    return new Proxy(obj, {
        get: function(target, key, receiver) {
        	// getter
            track(target, key); // Collect the effect function
            return Reflect.get(target, key, receiver);
        },
        set: function(target, key, value, receiver) {
        	// setter
            let oldValue = target[key]; / / the original value
            if (oldValue === value) {
                return;
            }
            let res = Reflect.set(target, key, value, receiver);
            trigger(target, key); // Trigger the effect function
            returnres; }}); }Copy the code

If you don’t know About Reflect, you won’t do it here. Track and trigger functions are then implemented

/ /...
const targetMap = new WeakMap(a);const effectStack = [];
let activeEffect = null;

function track(target, key) {
    if(! activeEffect) {return;
    }

    / / collection
    let depsMap = targetMap.get(target);
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
    }
    let dep = depsMap.get(key);
    if(! dep) { depsMap.set(key, (dep =new Set()));
    }
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
    }
}

function trigger(target, key) {
    / / triggers
    const depsMap = targetMap.get(target);
    if(! depsMap) {return;
    }

    let dep = depsMap.get(key);

    let effects = new Set(a); dep && dep.forEach((effect) = > effects.add(effect));

    / / execution
    effects.forEach((effect) = > {
        if (effect.options.scheduler) {
            effect.options.scheduler(effect);
        } else{ effect(); }}); }Copy the code

This is the general code of the reactive implementation. It’s not complete, but it’s understandable. Students who want to know more can go directly to vue-next source code.

5.2, the effect

Next comes the implementation of Effect (FN,options). The code is as follows:

const effectStack = [];
let activeEffect = null;
/ /...
function effect(fn, options = {}) {
    let effect = createEffect(fn, options);
    if(! options.lazy) { effect(); }return effect;
}

let uid = 0;
function createEffect(fn, options) {
    const effect = () = > {
        if(! effectStack.includes(effect)) {try {
                effectStack.push(effect);
                activeEffect = effect;
                // The collection is performed by default
                return fn();
            } finally {
                // The processing is complete
                effectStack.pop();
                activeEffect = effectStack[effectStack.length - 1]; }}}; effect.raw = fn; effect.id = uid++; effect.options = options;/ /...
    return effect;
}
Copy the code

Effect (fn,options) returns the result of fn(). And if you put reactive and effect together you can see how dependent they are. Proxy uses getter to collect activeEffects and setter to trigger activeeffects. Two seemingly independent methods are connected by the global variable activeEffect.

5.3, the computed

With reactive and effect implemented, computations are much simpler. Implementation code for computed:

/ /...
function computed(fn) {
    let runner = effect(fn, {
        lazy: true});return {
        get value() {
            // Perform the collection
            return runner();
        },
        set value(value) {}}; }/ / use
let data = reactive({ count : 0 })

let text = computed(() = > {
    return `count:${data.count}`;
});

console.log(text.value) // count:0
Copy the code

Yes, that’s right. The implementation of computed is that simple. Compared with computed and watch, the implementation is much more complicated.

5.3, watch

// This is just a simple implementation of watch for understanding

function watch(source, cb) {
    let getter = () = > {};
    if (isFunction(source)) {
        getter = source;
    }
    // Collect information
    let runner = effect(getter, {
        lazy: true.scheduler: () = > {
            // Perform the callback
            let value = runner();
            if(value ! == oldValue) { cb(oldValue, value); } oldValue = value; }});// Perform collection for the first time
    let oldValue = runner();
    // Stop listening
    return () = >{
    	stop(runner)
        / /...}}/ / use
let stopWatcher = watch(
    () = > data.count,
    (oldValue, value) = > {
        / / execution
        console.log("========watch========", oldValue, value); });// You can stop watch
// stopWatcher()

data.count = 100;
// ========watch======== 0 100

Copy the code

5.4 complete code

// reactive
function reactive(obj) {
    return new Proxy(obj, {
        get: function(target, key, receiver) {
            track(target, key);
            // getter
            return Reflect.get(target, key, receiver);
        },
        set: function(target, key, value, receiver) {
            let oldValue = target[key]; / / the original value
            if (oldValue === value) {
                return;
            }

            // setter
            let res = Reflect.set(target, key, value, receiver);
            trigger(target, key);
            returnres; }}); }const targetMap = new WeakMap(a);const effectStack = [];
let activeEffect = null;
// track
function track(target, key) {
    if(! activeEffect) {return;
    }

    / / collection
    let depsMap = targetMap.get(target);
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
    }
    let dep = depsMap.get(key);
    if(! dep) { depsMap.set(key, (dep =new Set()));
    }
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
    }
}
// trigger
function trigger(target, key) {
    / / triggers
    const depsMap = targetMap.get(target);
    if(! depsMap) {return;
    }

    let dep = depsMap.get(key);

    let effects = new Set(a); dep && dep.forEach((effect) = > effects.add(effect));

    / / execution
    effects.forEach((effect) = > {
        if (effect.options.scheduler) {
            effect.options.scheduler(effect);
        } else{ effect(); }}); }// effect
function effect(fn, options = {}) {
    let effect = createEffect(fn, options);
    if(! options.lazy) {// Execute the phone
        effect();
    }

    return effect;
}

let uid = 0;
function createEffect(fn, options) {
    const effect = () = > {
        if(! effectStack.includes(effect)) {try {
                effectStack.push(effect);
                activeEffect = effect;
                // Perform collection for the first time
                return fn();
            } finally {
                // The processing is complete
                effectStack.pop();
                activeEffect = effectStack[effectStack.length - 1]; }}}; effect.raw = fn; effect.id = uid++; effect.options = options;return effect;
}
// computed
function computed(fn) {
    let runner = effect(fn, {
        lazy: true});return {
        get value() {
            // Perform the collection
            return runner();
        },
        set value(value) {}}; }// watch
function watch(source, cb) {
    let getter = () = > {};
    if (typeof source === "function") {
        getter = source;
    }
    // Collect information
    let runner = effect(getter, {
        lazy: true.scheduler: () = > {
            // Perform the callback
            let value = runner();
            if(value ! == oldValue) { cb(oldValue, value); } oldValue = value }, });// Perform collection for the first time
    let oldValue = runner();
}
Copy the code

6, summary

The following points can be summarized through simple implementation:

  • The vue core is the core of rewriting getters and setters of new Proxy() to implement data-driven implementation
  • The implementation of computed and watch shows that the actual principle depends on the Effect () factory
  • ActiveEffect can be thought of as a transportation hub between New Proxy() and Effec ()

Once again: the above implementation code is only for understanding the implementation principle, detailed implementation please move to VUE-next source code.

If there are mistakes, welcome to correct them