MVVM

concept

MVVM represents the Model-view-ViewModel.

  • Model: The Model layer, which handles business logic and interacts with the server side
  • View: The View layer is responsible for transforming the data model into a UI presentation, which can be simply understood as an HTML page
  • ViewModel: the ViewModel layer, which connects the Model to the View. It is the communication bridge between the Model and the View

The View layer and the Model layer are not directly related, but interact through the ViewModel layer. The ViewModel layer connects the View layer with the Model layer by two-way data binding, making synchronization between the View layer and the Model layer completely automatic.

The methods and representatives of data binding are:

  • Publish subscription mode (Backbone)
  • Data hijacking or proxy (VueJS, AvalonJS) throughObject.definePropertyorProxyThe formerCannot listen for array changes.Each property of the object must be traversed.Nested objects must be traversed deeply; The latter can beListen for array changesStill,You need to go deep through nested objects, compatibility is less than the former.
  • Dirty data check (AngularJs, RegularJS) inWhen a UI change might be triggeredPerform dirty checks, such as DOM events, XHR response events, and timers.

implementation

Bidirectional data binding requires the implementation of the following three classes:

  • ObserverListener: To listen for property changes and notify subscribers
  • WatcherSubscribers: Receive notification of property changes and then update the view
  • CompileParser: parses instructions, initializes templates, and binds subscribers

Next, let’s implement a simple MVVM framework the way Vue is implemented.

  1. Implement a listener Observer

Using obeject.defineProperty () to listen for property changes, you recursively walk through the data object that requires observe, including the properties of the child property object, with setters and getters. When you assign a value to this object, that setter is triggered, so you can listen for data changes.

When we listen to a property change, we need to notify the Watcher subscriber to perform an update function to update the view. In this process, we may have many Watcher subscribers, so we need to create a container Dep to do a unified management.

function observer(data) {
  if(! data ||typeofdata ! = ='object') {return;
  }
  Object.keys(data).forEach(key= >{ defineReactive(data, key, data[key]); })}function defineReactive(obj,key,val){
  observer(val); // Listen to the child property recursively
  var dep = new Dep(); // Subscriber dependency collector

  Object.defineProperty(obj, key, {
     enumerable: true.configurable: true.get: function getter(){
        if (Dep.target) {
          dep.addSub(Dep.target); // Add each property to the listener in the getter
        }
        return val;
     },
     set: function setter(newVal){
        if (newVal === val) {
          return;
        }
        val = newVal; // Reassign the data

        console.log('Listening for a value change', val, '- >', newVal);
        dep.notify(); // Notify the subscriber}})}function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub);
}
Dep.prototype.notify = function () {
  console.log('Property changes, notifying Watcher to execute function to update view');
  this.subs.forEach(sub= > {
    sub.update(); // Call the Watcher view update function
  })
}
Dep.target = null; / / the global target
Copy the code

With a listener Observer created above, we can now add listeners to an object and change the properties to see what happens.

var person = {
  name: 'ben'
}
observer(person);
person.name = 'bob';
Copy the code

It prints out from the console: property changes that notify Watcher to execute a function that updates the view, proving that the listener Observer is in effect.

  1. Implement the subscriber Watcher

Now that the property changes have been heard, it’s time for the Watcher to perform the update. The Watcher is notified of property changes and then performs an update function to update the view.

function Watcher(vm, prop, callback) {
  this.vm = vm;
  this.prop = prop;
  this.callback = callback;
  this.value = this.getValue();// After the new Watcher, automatically add the listener
}
Watcher.prototype.update = function(){
  const value = this.vm.$data[this.prop];
  const oldVal = this.value;
  if(value ! == oldVal) {this.value = value;
    this.callback(value);
  }
}
Watcher.prototype.getValue = function(){
   Dep.target = this; // Store the subscriber
   const value = this.vm.$data[this.prop]; // Since the property is being listened on, this step will execute the listener's get method
   Dep.target = null;
   return value;
}
Copy the code

The above two steps have achieved a simple two-way binding, so let’s combine the two and see what happens.

function MVVM(options){
   this.$options = options || {};
   this.$data = this.$options.data;
   this.$el = document.querySelector(this.$options.el);
   this.init();
}
MVVM.prototype.init = function(){
   var prop = 'name' // Temporarily write the name of the property
   observer(this.$data)
   this.$el.innerText = this.$data[prop] 
   new Watcher(this, prop, value => {
     this.$el.innerText = value
   })
}
Copy the code

Verify:


      
<html lang="en">
<head></head>
<body>
<div id="app">{{name}}</div>
</body>
<script src="test.js"></script>
<script>
  const vm = new MVVM({
    el: "#app",
    data: {
      name: "ben"}})</script>
</html>
Copy the code

Ben is displayed on the page, and when we print vm.$data.name = ‘jack’ in the browsing console, jack is instantly displayed on the page. Thus a simple two-way data binding is implemented. But the name attribute is dead, and the el. InnerText is not extensible, so let’s implement a template parser.

  1. Implement the compiler

The main function of Compile is to parse instructions to initialize the template, add subscribers, and bind update functions. Since we frequently manipulate the DOM during the parsing of DOM nodes, we use document fragments to help us parse the DOM and optimize performance. First, the whole node and instructions are processed and compiled, different rendering functions are called according to different nodes, and update functions are bound. After compilation, DOM fragments are added to the page.

function Compile(vm) {
  this.vm = vm;
  this.el = vm.$el;
  this.fragment = null;
  this.init();
}
Compile.prototype = {
  init: function () {
    this.fragment = this.nodeFragment(this.el);
  },
  nodeFragment: function (el) {
    const fragment = document.createDocumentFragment();
    let child = el.firstChild;
    // Move all the child nodes in the document fragment
    while (child) {
      fragment.appendChild(child);
      child = el.firstChild;
    }
    return fragment;
  },
  compileNode: function (fragment) {
      let childNodes = fragment.childNodes;
      [...childNodes].forEach(node= > {
        let reg = / \ {\ {(. *) \} \} /;
        let text = node.textContent;
        if (this.isElementNode(node)) {
          this.compile(node); // Render the directive template
        } else if (this.isTextNode(node) && reg.test(text)) {
          let prop = RegExp. $1;
          this.compileText(node, prop); // Render the {{}} template
        }
  
        // Compile the child nodes recursively
        if (node.childNodes && node.childNodes.length) {
          this.compileNode(node); }}); },compile: function (node) {
      let nodeAttrs = node.attributes;
      [...nodeAttrs].forEach(attr= > {
        let name = attr.name;
        if (this.isDirective(name)) {
          let value = attr.value;
          if (name === "v-model") {
            this.compileModel(node, value); } node.removeAttribute(name); }}); },/ / to omit...
}
Copy the code

The MVVM function now looks like this

function MVVM(options){
   this.$options = options || {};
   this.$data = this.$options.data;
   this.$el = document.querySelector(this.$options.el);
   this.init();
}
MVVM.prototype.init = function(){
   observer(this.$data)
   new Compile(this);
}
Copy the code
  1. Adding a Data Broker

$data.name = ‘jack’; $data.name = ‘jack’; The answer is to add a layer of data broker.

function MVVM(options){
   this.$options = options || {};
   this.$data = this.$options.data;
   this.$el = document.querySelector(this.$options.el);
   // Data broker
   Object.keys(this.$data).forEach(key= > {
      this.proxyData(key);
   });

   this.init();
}
MVVM.prototype.init = function(){
   observer(this.$data)
   new Compile(this);
}
MVVM.prototype.proxyData = function (key) {
   Object.defineProperty(this, key, {
       get: function () {
         return this.$data[key]
       },
       set: function (value) {
         this.$data[key] = value; }}); }Copy the code

Vue summary

  • Each Vue Component has a corresponding Watcher instance.
  • Properties on Vue’s data will have getter and setter properties added.
  • When the Vue Component render function is executed, the data will be touched, that is, read, the getter method will be called, and Vue will record all the data that this Vue Component depends on. (This process is called dependency collection.)
  • When the data is changed (mainly by the user), that is, the setter method is called, and Vue informs all components that depend on the data to call their render function for the update.
  • Vue cannot detect the addition or removal of a property. Since Vue performs getter/setter transformations on the property when initializing the instance, the property must exist on the data object for Vue to convert it to responsive.
var vm = new Vue({
  data: {a:1}})// 'vm.a' is reactive

vm.b = 2
// 'vm.b' is non-responsive

Vue.set(vm.someObject, 'b'.2) // You can add reactive properties to nested objects via the vue.set (object, propertyName, value) method
Copy the code
  • Vue cannot detect changes to the following arrays:
  1. When you set an array item directly using an index, for example: vm.items[indexOfItem] = newValue
  2. When you change the length of an array, for example: vm.items.length = newLength
var vm = new Vue({
  data: {
    items: ['a'.'b'.'c']
  }
})
vm.items[1] = 'x' // Not responsive
vm.items.length = 2 // Not responsive

Vue.set(vm.items, indexOfItem, newValue) // Reactive status updates can be triggered by vue.set (vm.items, indexOfItem, newValue)
Copy the code
  • Vue in moreThe new DOM executes asynchronously. As soon as data changes are listened for, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is fired multiple times, it will only be pushed into the queue once. This removal of duplicate data at buffering time is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop “tick”, Vue refreshes the queue and performs the actual (deduplicated) work.