Learn about Vue - [how Vue implements responsive]

Keywords: Javascript Vue Attribute

Write in the front: This article is a summary of personal daily work and study, which is convenient for later checking and filling in the missing, non authoritative information. Please bring your own thinking.
Previous link: Learn about Vue - [how Vue implements responsive (I)].
Learn about Vue - [how Vue implements responsive (II)].
I've got some understanding of the response. Here, I'll try to write it by myself (copy it).
Set the object data as the response object to be defined and the key as the attribute in data.

  1. Define getter/setter for each property key in data through defineProperty, so that the corresponding operation will be triggered when data[key] is referenced and assigned, which is the basis of responsive implementation;
  2. dep object, as a dependent entity, contains methods such as dependency addition, watcher addition, and change notification, which are used for dependency collection and change distribution, and contains a list of observers.
  3. watcher object, observer object, including dependency collection and update methods; used for dependency collection and update, including dependency list;

Dep constructor

  let uid = 0;

  class Dep {
    constructor() {
      this.id = ++uid;
      this.subs = [];
    }
    addSub(target) { // Add target to the observer list
      this.subs.push(target);
    }
    depend() { // 
      if (Dep.target) {
        Dep.target.addDep(this);
      }
    }
    removeSub(target) {
      this.subs.splice(this.subs.findIndex(_ => _.id == target.id), 1);
    }
    notify() {
      const subs = this.subs.slice();
      for (let i = 0; i < subs.length; i++) {
        subs[i].update();
      }
    }
  }

  Dep.target = null; // Currently active watcher
  const targetStack = []; // The stack where the watcher is stored

  const pushTarget = target => { // Update the currently active Dep.target
    if (Dep.target) {
      targetStack.push(target); // Push target (watcher) into the stack
    }
    Dep.target = target;
  };

  const popTarget = () => { // Pop the top watcher from the watcher stack
    Dep.target = targetStack.pop();
  };

watcher constructor

  class Watcher {
    constructor(getter, options = {}) {
      this.deps = []; // Dependency list
      this.newDeps = []; // Last added dependency list
      this.depIds = new Set(); // List of dependent ids
      this.newDepIds = new Set(); // List of last added dependent ids
      this.getter = getter; // 
      this.lazy = !!options.lazy; // Lazy dependency, do not execute getter when instantiating for the first time
      this.dirty = this.lazy; // Dirty value identification, mainly used in computed calculation;
      this.lazy ? undefined : this.get();
    }
    get() {
      pushTarget(this); // Take the current watcher as the active watcher object and push it into the targetStack stack
      const value = this.getter();
      popTarget(); // Set the current watcher as the previous one on the stack
      this.cleanupDeps(); // Dependency collation, mainly used to collate this.deps, this.depIds
      return value;
    }
    addDep(dep) {
      if (!this.newDepIds.has(dep.id)) {
        this.newDepIds.add(dep.id);
        this.newDeps.push(dep);
        if (!this.depIds.has(dep.id)) {
          dep.addSub(this);
        }
      }
    }
    cleanupDeps() {
      let i = this.deps.length;
      while (i--) {
        const dep = this.deps[i];
        if (!this.newDepIds.has(dep.id)) { // If the new dependency list no longer contains the previous dependency, the dep.removeSub method is called to remove the current watcher from the dep.subs list.
          dep.removeSub(this);
        }
      }
      [this.deps, this.newDeps] = [this.newDeps, this.deps];
      this.newDeps.length = 0;
      [this.depIds, this.newDepIds] = [this.newDepIds, this.depIds];
      this.newDepIds.clear();
    }
    evaluate() {
      this.value = this.get();
      this.dirty = false;
    }
    update() {
      if (this.lazy) {
        this.dirty = true;
      } else {
        new Promise((resolve) => {
          resolve();
        }).then(() => {
          this.get();
        });
      }
    }
    depend() { // Add the dependency of the current watcher to the dependency list of the current Dep.target
      const deps = this.deps;
      for (let i = 0; i < deps.length; i++) {
        deps[i].depend();
      }
    }
  }

defineReactive method

Used to define getter/setter for data[key]

  const defineReactive = (target, key, val) => {
    const dep = new Dep(); // The instantiated dep object is used to access the object for dependency collection when the getter/setter is triggered. In essence, the currently instantiated dep corresponds to the current data[key] one by one.
    Object.defineProperty(target, key, {
      enumerable: true,
      configurable: true,
      get() {
        if (Dep.target) {
          dep.depend(); // Dependency adding
        }
        return val;
      },
      set(newVal) {
        if (val === newVal) return;
        val = newVal;
        dep.notify();
      },
    });
  };

data, computed initialization

  const initData = data => {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(vm, keys[i], data[keys[i]]);
    }
  };

  const initComputed = computed => {
    const keys = Object.keys(computed);
    for (let i = 0; i < keys.length; i++) {
      const userDef = computed[keys[i]].bind(vm);
      const watcher = new Watcher(userDef, { lazy: true });

      Object.defineProperty(vm, keys[i], {
        enumerable: true,
        configurable: true,
        get() {
          if (watcher) {
            if (watcher.dirty) {
              watcher.evaluate();
            }
            if (Dep.target) {
              watcher.depend();
            }
            return watcher.value;
          }
        },
      });
    }
  };

Simulation page rendering:

<!-- html -->
<body>
  <div id="app"></div>
  <button id="btn">To update</button>
</body>
  const data = initData(vm.data);

  const computed = initComputed(vm.computed);

  const updateComponent = () => {
    const app = document.querySelector('#app');
    var a = vm.current; // Reference vm.current
    var b = vm.computedCurrent; // Apply computed
    app.innerHTML = `data: <i>${a}</i> computed: <strong>${b}</strong>`;
  };

  defineReactive(vm, 'current', vm.current); // Define getter/setter for vm.current
  const watcher = new Watcher(updateComponent);

You can see that click the button to update the current attribute in vm, and the page has been updated successfully...

Process analysis

Initialize the data property getter / setter - > instantiate the watcher, with the update method, addDep method - > watcher.get method to perform dependency collection (the referenced data property in this method is regarded as the dependency of the current watcher) - > during the process of watcher dependency collection, the dependent data property will also be collected by the watcher -- > data property update -- > notify the watcher, and the update method will adjust. Use a new round of dependency collection, comparison of old and new dependencies, and delete the new dependency from the dependency list if the old dependency is missing, and add the observer watcher to the dependent subs watcher list at the same time to execute the business code (view update).

For computed, it is not only an observer, but also a dependency.

For a watch, it is an observer. It depends on the data or calculated to be watched. When it depends on an update, it will notify it and execute the corresponding method.

In principle, there are many details.

THE END

Posted by Mchl on Wed, 30 Oct 2019 20:19:09 -0700