At present, about 40% of the demand is in the Vue2 technology stack, the so-called know what is more to know why, in order to better use Vue, faster troubleshooting problems, recently learned some knowledge related to the source code, although the online summary of Vue a lot of a lot of their own, but not much of their own one, Welcome to discuss learning together, find problems welcome to point out.

What does a responsive system do

Back to the simplest code:

data = {
    text: 'hello, world'
}

const updateComponent = () = > {
    console.log('received', data.text);
}

updateComponent()

data.text = 'hello, liang'
// Run the result
// Receive hello, world
Copy the code

What a reactive system does: a function that depends on data is re-executed when the dependent data changes.

We expect the updateComponent function to be executed again when data.text is modified above.

To implement a responsive system, we need to do two things:

  1. Know which functions depend on the data in data

  2. Call functions that depend on data when the data in data changes

To implement point 1, we need to save the current function when the function is executed, and then save the function to the current data when the data is read.

The second point is to execute the saved function once when modifying the data.

To do something extra when reading and modifying data, we can override the get and set functions of Object attributes via Object.defineProperty().

Responsive data

Let’s write a function that overrides the get and set functions for attributes.

/** * Define a reactive property on an Object. */
export function defineReactive(obj, key, val) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    // Read user - defined get and set
    const getter = property && property.get;
    const setter = property && property.set;
    // Val does not pass in for manual assignment
    if((! getter || setter) &&arguments.length === 2) {
        val = obj[key];
    }

    Object.defineProperty(obj, key, {
        enumerable: true.configurable: true.get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
            // 1. The current function needs to be saved
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;

            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
            // 2. The current data-dependent function will be executed
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /}}); }Copy the code

To make it easier to call, we wrap the operations of steps 1 and 2 into a Dep class.

export default class Dep {
    static target; // The function currently being executed
    subs; // The dependent function
    constructor() {
        this.subs = []; // Save all functions that need to be executed
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    depend() {
        // Go here when trigger get
        if (Dep.target) {
            // Delegate to dep. target to call addSub
            Dep.target.addDep(this); }}notify() {
        for (let i = 0, l = this.subs.length; i < l; i++) {
            this.subs[i].update();
        }
    }
}

Dep.target = null; // Static variable, globally unique
Copy the code

We save the currently executing function to the target variable of the Dep class.

Save the currently executing function

To save the current function, we also need to write a Watcher class, pass in the function that needs to be executed, save it in the getter property of the Watcher class, and hand it over to the Watcher class to execute.

So in the Dep class, instead of holding the current function in subs, the Watcher object holds the current function.

import Dep from "./dep";
export default class Watcher {
    constructor(Fn) {
        this.getter = Fn;
        this.get();
    }

    /** * Evaluate the getter, and re-collect dependencies. */
    get() {
        Dep.target = this; // Save the Watcher that wraps the currently executing function
        let value;
        try {
          	// Invoke the currently passed function to trigger the get of the object property
            value = this.getter.call();
        } catch (e) {
            throw e;
        }
        return value;
    }

    /** * Add a dependency to this directive. */
    addDep(dep) {
      	// Get will go here and collect the current dependency
        // The Watcher of the currently executing function is saved to subs in the DEP
        dep.addSub(this);
    }

    /** * Subscriber interface. * Will be called when a dependency changes. */
  	// Set is triggered when the object property value is modified
    update() {
        this.run();
    }

    /** * Scheduler job interface. * Will be called by the scheduler. */
    run() {
        this.get(); }}Copy the code

The function is wrapped by Watcher and stored in dep. target. Then the function passed in is called.

If the value of an object property is changed in the future, the set of the object property is triggered, and the previously collected Watcher object is called, through the Uptate method of the Watcher object, to call the function that was originally executed.

Responsive data

Going back to the defineReactive function we left behind, let’s complete it in the same way we did above.

import Dep from "./dep";
/** * Define a reactive property on an Object. */
export function defineReactive(obj, key, val) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    // Read user - defined get and set
    const getter = property && property.get;
    const setter = property && property.set;
    // Val does not pass in for manual assignment
    if((! getter || setter) &&arguments.length === 2) {
        val = obj[key];
    }

    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    const dep = new Dep(); // Hold a Dep object that holds all Watcher dependencies on this variable
    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

    Object.defineProperty(obj, key, {
        enumerable: true.configurable: true.get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
            // 1. The current function needs to be saved
            if (Dep.target) {
                dep.depend();
            }
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;

            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
            // 2. The current data-dependent function will be executed
            dep.notify();
            / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /}}); }Copy the code

The Observer object

Let’s write an Observer method that makes all properties of the object responsive.

export class Observer {
    constructor(value) {
        this.walk(value);
    }

    /** * iterates through all the attributes of the object, calling defineReactive to intercept the get and set methods of the attributes of the object */
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); }}}Copy the code

We provide an observe method to create an Observer.

export function observe(value) {
    let ob = new Observer(value);
    return ob;
}
Copy the code

test

Apply the above method to the example at the beginning of this article and do it:

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    text: "hello, world"};// Make the data responsive
observe(data);

const updateComponent = () = > {
    console.log("Receive", data.text);
};

// The current function is executed by Watcher
new Watcher(updateComponent);

data.text = "hello, liang";
Copy the code

At this point, the output will be twice ~

Received hello, world received hello, liangCopy the code

It means our responsive system worked.

The total

Firstly, the whole process of responsive system is understood as follows:

Each property has a subs array, and the Watcher will hold the currently executing function. When the property is read, get will be triggered, and the current Watcher will be saved in the Subs array. When the property value is modified, the saved function will be executed through the Watcher object in the subs array.

Of course, there are still billions of details to be worked out, which will continue in the next article.