In-depth vue2.x source code series: handwritten code to simulate the responsive data implementation of Vue2.x

Foreword

The Vue responsive principle consists of the following three parts:

  1. Data hijacking: Vue intercepts each attribute in data through the Object.defineProperty() method. When the attribute value changes, it will trigger the setter method to notify the dependency update.
  2. Publish-subscribe model: Vue uses the publish-subscribe model to implement responsive updates of data. When the data changes, the dependency will be notified to update.
  3. Dependency collection: When Vue renders a component, it will collect the data used in the template and associate the data used in the component with the corresponding Watcher object. When the data changes, it will notify the related Watcher object to update .

Implementation process

The principle of handwritten Vue responsiveness can be divided into the following steps:

  1. Implement the Observer class: use the Object.defineProperty() method to intercept each property in the data. When the property value changes, the setter method will be triggered to notify the dependency update.
  2. Implement the Dep class: used to manage Watcher objects, including adding and deleting Watcher objects, and notifying Watcher objects to update.
  3. Implement the Watcher class: used to establish the connection between the view and the data. When the data changes, the Watcher object will be notified to update.
  4. Implement the Compile class: used to parse the template instructions, render the data corresponding to the instructions into the view, and establish the connection between the view and the data.
  5. Implement the Vue class: integrate the Observer, Watcher, and Compile classes to implement Vue’s responsive update mechanism.

In general, the principle of handwritten Vue responsiveness is mainly composed of Observer, Dep, Watcher, Compile, and Vue. Among them, Observer is used to intercept data changes, Dep is used to manage Watcher objects, Watcher is used to establish the connection between views and data, Compile is used to parse template instructions, and Vue integrates these classes to implement Vue’s responsive update mechanism.

Create a Dep class

We use recursion to traverse all properties in the data object, and define each property using the Object.defineProperty() method. In the defineReactive() method, we also created a Dep class, Dep class is used to manage all subscribers (Watcher) and notify them of updates.

class Dep {<!-- -->
  constructor() {<!-- -->
    this.subs = []; // Array to store dependencies
  }

  // add dependencies
  addSub(sub) {<!-- -->
    if (sub & amp; & amp; sub. update) {<!-- -->
      this.subs.push(sub);
    }
  }

  // Notify dependency update
  notify() {<!-- -->
    this.subs.forEach(sub => {<!-- -->
      sub. update();
    });
  }
}

Dep.target = null; // Static attribute target, used to save the current Watcher object

The above code defines a class called Dep, which has the following methods:

  1. constructor(): Constructor, initialize the subscriber array subs to an empty array.
  2. addSub(sub): The method of adding a subscriber, adding the incoming subscriber object sub to the subs array.
  3. notify(): A method to notify all subscribers of updates, traverse the subs array, and call its update() method for each subscriber.
  4. target: Define a global variable target to store the current subscriber object.

In Vue, each responsive data (such as an attribute in data) corresponds to a Dep object. When this property is read, the current subscriber object will be stored in Dep.target, and then in the getter method of the property, Dep.target will be added to In the subscriber array of the Dep object of the current attribute; when the value of the attribute is modified, notify()Dep object of the attribute will be called > method that notifies all subscribers of the update.

Create a Watcher class

Next, we need to create a Watcher class whose main function is to trigger the update operation of the view when the data changes. In the Watcher class, we first need to save the callback function required to update the view, and add the Watcher instance to the data subscription list. When the data changes, we traverse the subscription list and call the callback function in turn to update the view.

// Create a Watcher class to manage dependencies and view updates
class Watcher {<!-- -->
  constructor(vm, key, cb) {<!-- -->
      this.vm = vm;
      this.key = key;
      this.cb = cb;
      // Specify the current Watcher instance as Dep.target
      Dep. target = this;
      // Get the value of the data, trigger the get method of the data, and add the current Watcher instance to Dep
      this. oldValue = vm[key];
      Dep. target = null;
  }

 
// update view
update() {<!-- -->
    const newValue = this. vm[this. key];
    if (this. oldValue === newValue) {<!-- -->
        return;
    }
    this.cb(newValue);
    this. oldValue = newValue;
    }
}

Create an Observer class

Observer class: This class is used to monitor and respond to data, and mainly implements two methods: walk and defineReactive. The walk method traverses all properties in the object, and calls the defineReactive method for each property to perform responsive processing; the defineReactive method uses Object.defineProperty to add getters and setters to each property. When the property is accessed or modified, the corresponding dependency update will be triggered.

class Observer {<!-- -->
  constructor(data) {<!-- -->
    this. walk(data);
  }

  // Recursively traverse the data object, adding getters and setters for each property
  walk(data) {<!-- -->
    Object.keys(data).forEach(key => {<!-- -->
      this.defineReactive(data, key, data[key]);
    });
  }

  defineReactive(obj, key, val) {<!-- -->
    const dep = new Dep(); // create a dependency collector
    Object.defineProperty(obj, key, {<!-- -->
      enumerable: true, // enumerable
      configurable: true, // configurable
      get() {<!-- -->
        // add dependencies
        if (Dep. target) {<!-- -->
          dep. depend();
        }
        return val;
      },
      set(newVal) {<!-- -->
        if (val === newVal) {<!-- -->
          return;
        }
        val = newVal;
        // Trigger dependency update
        dep. notify();
      }
    });
  }
}

Create a Compile class

Compile class code. In the Compile class, we first need to traverse the nodes in the template and process them according to their type. For ordinary nodes, we will process their text content, and for nodes containing instructions, we will create Watcher instances and add them to the subscription list.

class Compile {
  constructor(el, vm) {
    this.el = document.querySelector(el); // Get the root node
    this.vm = vm; // save the Vue instance
    this.compile(this.el); // Compile the template
  }

  compile(el) {
    const childNodes = el.childNodes; // Get the list of child nodes of the root node

    Array.from(childNodes).forEach(node => {
      if (node.nodeType === 1) { // element node
        this. compileElement(node);
      } else if (this.isInterpolation(node)) { // text node and contains interpolation syntax
        this. compileText(node);
      }

      // Recursively compile child nodes
      if (node.childNodes & amp; & amp; node.childNodes.length > 0) {
        this. compile(node);
      }
    });
  }

  compileElement(node) {
    const attrs = node.attributes; // Get the attribute list of the element node

    Array.from(attrs).forEach(attr => {
      const attrName = attr.name;
      const exp = attr. value;

      if (attrName.startsWith("v-")) { // match command
        const dir = attrName.substring(2); // Get the instruction name
        this[dir] & amp; & amp; this[dir](node, exp); // call the corresponding instruction function
      }
    });
  }

  compileText(node) {
    const exp = node.textContent; // Get the expression in the interpolation syntax
    node.textContent = this.getVMValue(exp); // replace the interpolation syntax with the value of the expression
  }

  isInterpolation(node) {
    return node.nodeType === 3 & amp; & amp; /\{\{(.*)\}\}/.test(node.textContent); // Text node with interpolation syntax
  }

  getVMValue(exp) {
    let value = this.vm;
    exp.split(".").forEach(key => {
      value = value[key];
    });
    return value;
  }

  // v-model directive
  model(node, exp) {
    this.bind(node, exp, "model");

    node.addEventListener("input", e => {
      const newValue = e. target. value;
      this.setVMValue(exp, newValue);
    });
  }

  // v-bind directive
  bind(node, exp, dir) {
    const updaterFn = this[dir + "Updater"];
    updaterFn & amp; & amp; updaterFn(node, this.getVMValue(exp));

    new Watcher(this. vm, exp, value => {
      updaterFn & amp; & amp; updaterFn(node, value);
    });
  }

  // model directive updates the view
  modelUpdater(node, value) {
    node.value = value;
  }

  // v-text directive
  text(node, exp) {
    this.bind(node, exp, "text");
  }

  // The text command updates the view
  textUpdater(node, value) {
    node.textContent = value;
  }

  setVMValue(exp, value) {
    let vm = this.vm;
    const keys = exp. split(".");
    keys.forEach((key, index) => {
      if (index < keys. length - 1) {
        vm = vm[key];
      } else {
        vm[key] = value;
      }
    });
  }
}

The instantiation of the Compile class needs to pass in two parameters: el and vm. Among them, el is the selector of the root node, and vm is the Vue instance.

The Compile class mainly implements the following functions:

  1. Traverse the root node and its child nodes, and compile each element node and text node containing interpolation syntax.
  2. For element nodes, traverse its attribute list, match instructions and call corresponding instruction functions for processing.
  3. For text nodes containing interpolation syntax, replace with the value of the expression.
  4. Implemented handling of v-model, v-bind and v-text directives.
  5. The processing of responsive data is realized, the data is monitored through Watcher, and the view is automatically updated when the data changes.

Create a complete Vue instance

Create a Vue class that combines the Observer, Watcher and Compile classes to create a complete Vue instance.

class Vue {<!-- -->
  constructor(options) {<!-- -->
    this. $options = options;
    this. $data = options. data;
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    // Proxy the properties of the Vue instance to the $data object
    this._proxyData(this. $data);
    // Create an Observer instance to monitor data changes
    new Observer(this. $data);
    // Create a Compile instance and parse template directives
    new Compile(this. $el, this);
  }
  //Use the _proxyData() method to proxy the data to the Vue instance, and then you can access the data through this.key in the Vue instance
  _proxyData(data) {<!-- -->
    Object.keys(data).forEach((key) => {<!-- -->
      Object.defineProperty(this, key, {<!-- -->
        enumerable: true,
        configurable: true,
        get() {<!-- -->
          return data[key];
        },
        set(newValue) {<!-- -->
          if (newValue === data[key]) {<!-- -->
            return;
          }
          data[key] = newValue;
        },
      });
    });
  }
}

So far, we have completed the process of handwriting code to simulate Vue2.0’s responsive data implementation. Through this process, we can gain a deep understanding of the responsive data principle of Vue2.0, so as to better apply Vue2.0 to develop applications.

The other source code series of vue2.0 will continue to be updated in the future, including the source code of vue3.0 that is currently being studied, and will be updated later. If you like it, pay attention.

In-depth vue2.0 source code series: handwritten code to simulate the responsive data implementation of Vue2.0

In-depth vue2.0 source code series: the realization principle of handwritten code simulating Vue2.0 to realize virtual DOM

In-depth vue2.0 source code series: the realization of life cycle

In-depth vue2.0 source code series: the implementation principle and implementation method of instructions

In-depth vue2.0 source code series: the realization principle of template compilation

In-depth vue2.0 source code series: the realization and application of event mechanism

In-depth vue2.0 source code series: from the perspective of source code to see the realization of MVVM architecture pattern

In-depth vue2.0 source code series: implementation of dependency tracking and computed properties