Vue responsive Principle & how to realize MVVM bidirectional binding

Posted by samba_bal on Fri, 15 Nov 2019 10:10:21 +0100

Preface

As we all know, the response of Vue.js is to use the data hijacking + publish subscribe mode. However, it means that as a Xiaobai, he always feels that he can answer it, and finally stops talking and ends up failing. As one of the classic interview questions, in most cases, he can only answer the question of "Object.defineProperty..."

So write this to sort out the responsive thinking for yourself

What is MVVM

Model, View, View model is the meaning of mvvm;

  • View binds events to the Model through DOM Listeners of view Model
  • Model manages data in View through Data Bindings
  • View model plays a role of connecting bridge

Response type

According to the mvvm model, when the model (data) changes, the corresponding view will also change automatically, which is called responsive
Let's take a picture.

// html

<div id="app">
  <input type="text" v-model='c'>
  <p>{{a.b}}</p>
  <div>my message is {{c}}</div>
</div>
// js

let mvvm = new Mvvm({
  el: '#app',
  data: {
    a: {
      b: 'This is an example'
    },
    c: 10,
  }
});

principle

When a vue instance is created, vue will traverse the properties of data options, use Object.defineProperty to convert them to getter/setter and track the related dependencies internally, and notify the changes when the properties are accessed and modified.
Each component instance / element has a corresponding watcher program instance, which records the attribute as a dependency during component rendering. Then when the setter of the dependency is called, the Watcher will be notified to recalculate, so that its associated components can be updated


To sum up, the most important is three steps

  • Data hijacking: use Object.defineProperty to set getter/setter for each data
  • Data rendering: add a watcher (dependent) for each component of the data used by the page
  • Publish and subscribe: add a subscriber (dependent collector) dep for each data, and add the corresponding observer to the dependency list. Whenever the data is updated, the subscriber (dependent collector) notifies all the corresponding observers (dependencies) to automatically update the corresponding page

Implement an MVVM

thinking

Through the above, we know about the operation principle of mvvm. Corresponding to the above, we can realize its functions respectively
1. A data listener, Observer, monitors all attributes of data, and informs subscribers of dep if there is any change
2. An instruction parsing / renderer Compile, which scans and parses the instructions of each element node, corresponding to the replacement data, and binding the corresponding update function
3. A dependency Watcher class and a dependency collector dep class
4. An mvvm class

Mvvm

We want to build an MVVM, according to the previous example of our MVVM

class Mvvm {
  constructor(option) {
    this.$option = option;
    // Initialization
    this.init();
  }

  init() {
    // Data monitoring
    observe(this.$option.data);
    // Compile
    new Compile(this.$option.el);
  }
}

Here I write only one function, and it's OK to write with a class

/* observe Monitor function, monitor all data in data and hijack data
 * @params
 * $data - mvvm data in the instance
 */
function observe(data) {
  // Judge whether it is an object
  if (typeof data !== 'object') return
  // Circular data
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key]);
  })

  /* Data hijacking defineReactive
   * @param
   * obj - Listener object; key - key of traversal object; val - Val of traversal object
   */
  function defineReactive(obj, key, val) {
    // Recursive child property
    observe(val);
    // Data hijacking
    Object.defineProperty(obj, key, {
      enumerable: true, // enumerable
      configurable: true, // Modifiable
      // Set getter and setter functions to hijack data
      get() {
        console.log('get!', key, val);
        return val
      },
      set(newVal) {
        // Monitor new data
        observe(newVal);
        console.log('set!', key, newVal);
        val = newVal; // assignment
      },
    })
  }
}

Instruction parsing

/* Compile Class to parse instructions on all nodes in the dom
 * @params
 * $el - Labels to render
 * $vm - mvvm Example
 */
class Compile {
  constructor(el, vm) {
    this.vm = vm;
    this.$el = document.querySelector(el); // Easy to mount to compilation instance
    this.frag = document.createDocumentFragment(); // Using fragment class for dom operation to save cost
    this.reg = /\{\{(.*?)\}\}/g;

    // Move all dom nodes into frag
    while (this.$el.firstChild) {
      let child = this.$el.firstChild;
      this.frag.appendChild(child);
    }
    // Compile element node
    this.compile(this.frag);
    this.$el.appendChild(this.frag);
  }
}

Such a compiled function framework is written, and then the detailed function functions in it need to be supplemented;
Because we need to recognize the {{xxx}} interpolation on the text node when looping the nodes...

class Compile {
  ...
  // Compile
  compile(frag) {
    // Traverse frag node
    Array.from(frag.childNodes).forEach(node => {
      let txt = node.textContent;
      
      // Compile text {}}
      if (node.nodeType === 3 && this.reg.test(txt)) {
        this.compileTxt(node, RegExp.$1);
      }

      // Recursive child node
      if (node.childNodes && node.childNodes.length) this.compile(node)
    })
  }

  // Compile text node
  compileTxt(node, key) {
    node.textContent = typeof val === 'undefined' ? '' : val;
  }
  ...
}

Here, when rendering the page for the first time, mvvm can render the data in the instance, but it is not enough, because we need it to be able to update automatically in real time

Publish subscribe

When a data is referenced by multiple nodes / components on a node at the same time, how can we automatically update the page one by one when the data is updated? This requires the publish and subscribe mode;
We can add an observer (dependent) watcher for each component of data used by the page during compilation;
Then add a subscriber (dependency collector) dep for each data, and add the corresponding observer (dependency) watcher to the dependency list. Whenever the data is updated, the subscriber (dependency collector) notifies all the corresponding observers (dependencies) to automatically update the corresponding page
So you need to create a Dep that can be used to collect, delete, and send messages to dependencies

Dep

class Dep {
  constructor() {
    // Create an array to hold all dependent paths
    this.subs = [];
  }
  // Add dependency @ sub - dependency (watcher instance)
  addSub(sub) {
    this.subs.push(sub);
  }
  // Reminder release
  notify() {
    this.subs.forEach(el => el.update())
  }
}

Watcher

// Observer / dependency
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // Get current data value at initialization
    this.value = this.get(); 
  }
  /* Get current value
   * @param $boolean: true - Data update / false - initialization
   * @return Current vm[key]
   */
  get(boolean) {
    Dep.target = boolean ? null : this; 
    // Trigger getter and add yourself to dep
    let value = UTIL.getVal(this.vm, this.key);
    Dep.target = null;
    return value;
  }
  update() {
    // Get the latest value; / / triggered only when initializing, but not when updating
    let nowVal = this.get(true);
    // Contrast old values
    if (this.value !== nowVal) {
      console.log('update')
      this.value = nowVal;
      this.cb(nowVal);
    }
  }
}

Back to Compile, we need to create a wacther instance for this component at the time of the first rendering;
Then the function of rendering update is put into the watcher's cb;

class Compile{
  ...
  // Compile text node
  compileTxt(node, key) {
+   this.bind(node, this.vm, key, 'text');
  }

+ // Binding dependency
+ bind(node, vm, key, dir) {
+   let updateFn = this.update(dir);
+   // First render
+   updateFn && updateFn(node, UTIL.getVal(vm, key));
+   // Set up observers
+   new Watcher(vm, key, (newVal) => {
+     // Rendering after cb
+     updateFn && updateFn(node, newVal);
+   });
+ }

+ // To update
+ update(dir) {
+   switch (dir) {
+     case 'text': // Text update
+       return (node, val) => node.textContent = typeof val === 'undefined' ? '' : val;
+       break;
+   }
+ }
  ...
}

After that, go back to the original define reactive, modify it, and add a dep instance for each data;
Add dependency for dep instance in getter, add publishing function of DEP instance in setter;

function observe(data) {
  ...
  function defineReactive(obj, key, val) {
    // Recursive child property
    observe(val);
    // Add dependent collector
+   let dep = new Dep();
    // Data hijacking
    Object.defineProperty(obj, key, {
      enumerable: true, // enumerable
      configurable: true, // Modifiable
      get() {
        console.log('get!', key, val);
        // Add subscription
+       Dep.target && dep.addSub(Dep.target);
        return val
      },
      set(newVal) {
        observe(newVal);
        console.log('set!', key, newVal);
        val = newVal;
        // Release update
+       dep.notify(); // Trigger update
      },
    })
  }
}

So far, a simple responsive Mvvm has been implemented. Whenever we modify the data, the corresponding page content will automatically re render and update;
So how is bi-directional binding implemented?

Bidirectional binding

Bi directional binding is to identify the element nodes of the node when compiling. If there is a v-model instruction, bind the value value value of the element and the response data, and add the corresponding value update method in the update function

class Compile {
  // Compile
  compile(frag) {
    // Traverse frag node
    Array.from(frag.childNodes).forEach(node => {
      let txt = node.textContent;

      // Compile element node
+     if (node.nodeType === 1) {
+       this.compileEl(node);
+     // Compile text {}}
      } else if (node.nodeType === 3 && this.reg.test(txt)) {
        this.compileTxt(node, RegExp.$1);
      }

      // Recursive child node
      if (node.childNodes && node.childNodes.length) this.compile(node)
    })
  }
  ...
+ compileEl(node) {
+   // Find command v-xxx
+   let attrList = node.attributes;
+   if (!attrList.length) return;
+   [...attrList].forEach(attr => {
+     let attrName = attr.name;
+     let attrVal = attr.value;
+     // Judge whether there is a 'v -' command
+     if (attrName.includes('v-')) {
+       // Compile instruction / bind tag value and corresponding data
+       this.bind(node, this.vm, attrVal, 'model');
+       let oldVal = UTIL.getVal(this.vm, attrVal); // Get the current value of vm instance
+       // Add input event monitoring
+       node.addEventListener('input', e => {
+         let newVal = e.target.value; // Get the new value of the input
+         if (newVal === oldVal) return;
+         UTIL.setVal(this.vm, attrVal, newVal);
+         oldVal = newVal;
+       })
+     }
+   });
+ }
  ...
  // To update
  update(dir) {
    switch (dir) {
      case 'text': // Text update
        return (node, val) => node.textContent = typeof val === 'undefined' ? '' : val;
        break;
+     case 'model': // model instruction update
+       return (node, val) => node.value = typeof val === 'undefined' ? '' : val;
+       break;
    }
  }
}

In short, two-way data binding is to add a listening function of addeventlister to a v-xxx instruction component. Once an event occurs, a setter is called, thus calling dep.notify() to notify all dependent watchers to call watcher.update() for update

summary

The process of implementing Mvvm is as follows

  • Using get and set of Object.defineProperty to hijack data
  • Use observe to traverse data data for listening, and create dep instance for data to collect dependency
  • Compile all nodes in dom with compile, and add wathcer instance for component
  • Synchronization of data and view through dep & watcher publish and subscribe mode

Project source code

Welcome to the project Source code

Last

Thank you for reading.
Welcome to correct and discuss
Welcome to star

Topics: Javascript Vue Attribute Fragment