Handwriting a complete set of Vue-based MVVM principles

As a front-end interviewer, I have to ask the following questions: Describe your understanding of MVVM?

Next, I will implement a complete set of Vue-based MVVMs from scratch, which will be provided to the next year's "Gold, Three, Silver, Four" Job-hopping peak buddies to read in detail about their own understanding of MVVM.

What is MVVM

Before we get to know MVVM, let's explain it to MVC.The MVC architecture existed initially and now on the back end.MVC represents three layers of the background, M represents the model layer, V represents the view layer, and C represents the controller layer. These three layers of architecture can completely meet the business needs development of most divisions.

MVC & Three-tier Architecture

Take Java as an example to illustrate the meaning and responsibilities of each layer in MVC and the three-tier architecture:

  1. Model: Model layer, representing each Java Bean.It is divided into two categories, one called data-hosting beans and the other called business-processing beans.
  2. View: The view layer, representing the corresponding view page, interacts directly with the user.
  3. Controller: The control layer, which is the "middleman" of Model and View, which forwards user requests to the appropriate Models for processing and processes the results of Model calculations to provide the appropriate response to the user.

Taking login as an example, this paper introduces the logical relationship among the three layers.When the user clicks the login button on the View view page, the login interface in the Controller control layer is invoked.In general, there are not many specific business logic codes written in the Controller layer, but only an interface method, which implements the specific logic in the Service layer, and then the specific logic in the service layer invokes the Model model in the DAO layer to achieve dynamic effect.

Description of MVVM

MVVM design pattern is evolved from MVC (originally from back-end), MVP and other design patterns.

  1. M-Data Model, simple JS object
  2. VM - ViewModel, which connects Model to View
  3. V - View layer, the DOM rendering interface presented to the user

    From the above MVVM schema diagram, we can see that the most core is ViewModel, which plays a major role: monitoring DOM elements in View and binding data in Model. When the View changes will cause data changes in Modal, data changes in Model will trigger View Rendering, thus achieving the effect of data two-way binding. This effect is also the core of Vue.Sex.

Common libraries implement two-way data binding:

  • Publish Subscription Mode (Backbone.js)
  • Dirty Value Check (Angular.js)
  • Data Hijacking (Vue.js)

When an interviewer answers the Vue's two-way data binding principle, almost everyone will say: Vue uses data hijacking combined with publishing and subscription mode, hijacks getters, setters of various attributes through Object.defineProperty(), publishes messages to subscribers when data changes, triggers corresponding callback functions, and implements data two-way binding.But continue to ask:

  • What core modules are required to implement an MVVM?
  • Why do the DOM operations take place in memory?
  • What is the relationship between the core modules?
  • How do I hijack an array in Vue?
  • Have you fully implemented an MVVM yourself?
  • ...

Next, I will implement a complete set of MVVMs step by step, and once again I ask MVVM-related questions, I will be able to stand out in the interview process.Before you begin writing MVVM, it is important to familiarize yourself with the core API s and publishing subscription patterns:

Describe the use of Object.defineProperty

The purpose of Object.defineProperty(obj, prop, desc) is to define a new property directly on an object or to modify an existing property

  1. obj: Current object whose properties need to be defined
  2. prop: Property name currently required to be defined
  3. desc:Attribute descriptor

Note: Attributes of objects can be modified or deleted by assigning values to their attributes, but attributes can be defined by Object.defineProperty(), and more precise control of object attributes can be achieved by setting descriptors.

let obj = {}
Object.defineProperty(obj, 'name', {
    configurable: true,   // Default to false, configurable Delete
    writable: true,       // Default to false, Writable [Modify]
    enumerable: true,     // Default is false, can you enumerate [for in traversal]
    value: 'sfm',         // The value of the name attribute
    get() {
        // get function is called when obj.name is obtained
    },
    set(val) {
        // val is a reassigned value
        // The set function is called when obj.name is reassigned
    }
})

Note: When get and set functions appear, the writable, enumerable attributes cannot appear at the same time, otherwise the system will error.The API does not support versions below IE8, that is, Vue is not compatible with browsers below IE8.

DocumentFragment - Document Fragmentation

DocumentFragment s represent document fragments, which are not part of the DOM tree, but can store the DOM and add the stored DOM to the specified DOM node.Then someone will ask, what's the use of it, can't you just add elements to the DOM?The reason for using it is that it performs much better on DOM than on DOM directly.

Introduction to publish subscription mode

The publisher-subscriber model defines a one-to-many dependency, in which when an object's state changes, all dependent objects are notified and updated automatically, resolving the functional coupling between the principal object and the observer.The following is a small example of a publishing subscription pattern, which can actually be understood as relying on an array relationship, where a subscription is put into a function, and a publication is a function in an array.

// Publish subscription mode has subscriptions before publishing
function Dep() {
    this.subs = [];
}
// Subscribe
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub);
}
Dep.prototype.notify = function() {
    this.subs.forEach(sub => sub.update());
}
// Watcher class, instances created from this class have update methods
function Watcher(fn) {
    this.fn = fn;
}
Watcher.prototype.update = function() {
    this.fn();
}
let watcher1 = new Watcher(function() {
    console.log(123);
})
let watcher2 = new Watcher(function() {
    console.log(456);
})
let dep = new Dep();
dep.addSub(watcher1); // Put watcher in the array
dep.addSub(watcher2);
dep.notify();

// Console Output:
// 123 456

Implement your own MVVM

In order to achieve the two-way binding of mvvm, the following points must be implemented:

  1. Implement a data hijack - Observer that monitors all attributes of the data object and notifies subscribers of any changes
  2. Implement a template compilation-Compiler that scans and parses instructions for each element node, replaces data according to the instruction template, and binds corresponding update functions
  3. Implement a - Watcher, which serves as a bridge between Observer and Compile, to update the view by subscribing to and receiving notifications of each property change, executing the corresponding callback function for the instruction binding
  4. MVVM as an entry function integrates the above three

Data Hijacking - Observer

The primary purpose of the Observer class is to hijack all levels of data within the data so that it has the ability to listen for changes in object attributes

[Key]

  1. When an object's attribute value is also an object, its value should also be hijacked - recursive
  2. When the object assigns the same value as the old one, no subsequent action is required - preventing duplicate rendering
  3. When template rendering gets object properties it calls get to add target, object property changes notify subscribers of updates - data changes, view updates
// Data hijacking
class Observer {
    constructor(data) {
        this.observer(data);
    }
    observer(data) {
        if(data && typeof data == 'object') {
            // Judging that data exists and that data is an object
            for(let key in data) {
                this.defineReactive(data, key, data[key]);
            }
        }
    }
    defineReactive(obj, key, value) {
        let dep = new Dep();
        this.observer(value); // If value is still an object, you need to see
        Object.defineProperty(obj, key, {
            get() {
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set:(newVal) => { // Set New Value
                if(newVal != value) { // New and old values do not need to be replaced if they are consistent
                    this.observer(newVal); // If the assignment is also an object, it needs to be observed
                    value = newVal;
                    dep.notify(); // Notify all subscribers that updates have been made
                }
            }
        })
    }
}

Note: This class only hijacks objects and does not listen on arrays.

Template Compiler

Compiler is a parse template directive that replaces variables in the template with data, then initializes the rendered page view, binds the update function to the node corresponding to each directive, adds subscribers listening to the data, and updates the view when the data changes

Compiler does three main things:

  • Traverse all child nodes of the current root node into memory
  • Compile document fragments to replace data for attributes in template (element, text) nodes
  • Write back the compiled content to the real DOM

[Key]

  1. Move the real dom into memory first - Document fragmentation
  2. Compile Element Node and Text Node
  3. Add observers to expressions and attributes in templates
// Template Compilation
class Compiler {
    /**
     * @param {*} el Element Note: There may be an'#app'string or a document.getElementById('#app') in the el option
     * @param {*} vm Example
     */
    constructor(el, vm) {
        // Determine whether the el attribute is an element or not
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        // console.log(this.el); get the current template
        this.vm = vm;
        // Get elements from the current node into memory to prevent page redrawing
        let fragment = this.node2fragment(this.el);
        // console.log(fragment); all nodes in memory

        // 1. Compile Templates Compile with data from data
        this.compile(fragment);
        // 2. Replace the contents in memory
        this.el.appendChild(fragment);
        // 3. Write the replacement back to the page
    }
    /**
     * Judgment is directive
     * @param {*} attrName Property name type v-modal
     */
    isDirective(attrName) {
        return attrName.startsWith('v-'); // Does it contain v-
    }
    /**
     * Compile Element Node
     * @param {*} node Element Node
     */
    compileElement(node) {
        // Gets the attributes of the current element node; [class array] NamedNodeMap; if there are no attributes, NamedNodeMap{length: 0}
        let attributes = node.attributes;
        // Array.from(), [...xxx], [].slice.call, and so on, can all convert an array of classes into a real array
        [...attributes].forEach(attr => {
            // attr format: type="text" v-modal="obj.name"
            let {name, value: expr} = attr;
            // Determine if it is an instruction
            if(this.isDirective(name)) { // v-modal v-html v-bind
                // console.log('element', node); element
                let [, directive] = name.split('-'); // Get Instruction Name
                // Different instructions need to be invoked to process
                CompilerUtil[directive](node, expr, this.vm);
            }
        });
    }
    /**
     * Compile the text node to determine if the contents of the current text node contain {{}}
     * @param {*} node Text Node
     */
    compileText(node) {
        let content = node.textContent;
        // console.log(content,'content'); content in an element
        if(/\{\{(.+?)\}\}/.test(content)) { // Regular matching requires only {{}} braces, empty does not need to get the middle of the braces
            // console.log(content,'content'); contains only {{}} no empty and other child elements without {{}}
            CompilerUtil['text'](node, content, this.vm);
        }
    }
    /**
     * Compile DOM Nodes in Memory
     * @param {*} fragmentNode Document fragmentation
     */
    compile(fragmentNode) {
        // Get the child node from the document fragment Note: childNodes [contains the first layer, does not contain {{}}, etc.)
        let childNodes = fragmentNode.childNodes; // Get the class array NodeLis
        [...childNodes].forEach(child => {
            // Is it an element node
            if (this.isElementNode(child)) {
                this.compileElement(child);
                // If it's an element, you need to pass yourself in and iterate through the child nodes recursively
                this.compile(child);
            } else {
                // Text Node
                // console.log('text', child);
                this.compileText(child);
            }
        });
    }
    /**
     * Place elements in nodes in memory
     * @param {*} node node
     */
    node2fragment(node) {
        // Create a stable fragment; the purpose is to write each child in this node to this document fragment
        let fragment = document.createDocumentFragment();
        let firstChild; // The first child in this node
        while (firstChild = node.firstChild) {
            // AppndChild is mobile, and every time a node is moved into memory, there will be one fewer node on the page
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
    /**
     * Determine if it is an element
     * @param {*} node Node of the current element
     */
    isElementNode(node) {
        return node.nodeType === 1;
    }
}
// Compile function
CompilerUtil = {
    /**
     * Get the corresponding data from the expression
     * @param {*} vm 
     * @param {*} expr 
     */
    getVal(vm, expr) {
        return expr.split('.').reduce((data, current) => {
            return data[current];
        }, vm.$data);
    },
    setVal(vm, expr, value) {
        expr.split('.').reduce((data, current, index, arr) => {
          if (index === arr.length - 1) {
            return data[current] = value;
          }
          return data[current]
        }, vm.$data)
    },
    /**
     * Processing v-modal
     * @param {*} node Corresponding Node
     * @param {*} expr Expression
     * @param {*} vm Current instance
     */
    modal(node, expr, vm) {
        // Give the input box the value attribute node.value = xxx
        let fn = this.updater['modalUpdater'];
        new Watcher(vm, expr, (newValue) => {//Adding an observer data update to the input box triggers the method to assign a new value to the input box
          fn(node, newValue)
        })
        node.addEventListener('input', e => {
          let value = e.target.value; // Get user input
          this.setVal(vm, expr, value);
        })
        let value = this.getVal(vm, expr); // Return tmc
        fn(node, value);
    },
    text(node, expr, vm) {
        let fn = this.updater['textUpdater'];
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // Add an observer to each {{}} of the expression
            new Watcher(vm, args[1], (newValue) => {
                fn(node, this.getContentValue(vm, expr)); // A new string was returned
            })
            return this.getVal(vm, args[1].trim());
        });
        fn(node, content);
    },
    updater: {
        // Insert data into nodes
        modalUpdater(node, value) {
            node.value = value;
        },
        // Processing text nodes
        textUpdater(node, value) {
            node.textContent = value;
        }
    }
}

Complier has the ability to parse HTML templates into Document Fragment s and creates a responsive Watcher that changes the data bound in the view.

Publish Subscription - Watcher

Watcher subscribers, as bridges between Observer and Comppile, do the following:

  1. Add yourself to the property subscriber (dep) when instantiating yourself
  2. You must have an update() method of your own
  3. If dep.notice() is notified of an attribute change and can call its own update() method and trigger callbacks bound in Compile, it will be successful.
// publish-subscribe
function Dep() { 
    this.subs = []
}
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub)
}
Dep.prototype.notify = function() {
    this.subs.forEach(sub => sub.update())
}
/**
  * watcher
  * @param {*} vm Current instance
  * @param {*} exp Expression
  * @param {*} fn Listening Functions
  */
function Watcher(vm, exp, fn) { 
    this.fn = fn;
    this.vm = vm;
    this.exp = exp; // Add to Contract
    Dep.target = this;
    let val = vm;
    let arr = exp.split('.');
    arr.forEach(function (k) { 
        val = val[k];
    })
    Dep.target = null; // Ensure that watcher s are not added repeatedly
}
Watcher.prototype.update = function() {
    let val = this.vm;
    let arr = this.exp.split('.');
    arr.forEach(function (k) { 
        val = val[k];
    })
    this.fn(val)
}

Dep and Watcher are simple implementations of the observer mode, Dep is a subscriber, manages all observers, and has the ability to send messages to observers.Watcher is the observer, who makes their own updates when they receive messages from subscribers.

Integration - MVVM

MVVM acts as the entrance to data binding, integrates Observer, Compile and Watcher, monitors the changes of model data through Observer, parses compile template instructions through Compile, and finally uses Watcher to bridge the communication between Observer and Compile to achieve data change->view update; view interaction change->two-way binding effect of data model change.

class MVVM {
    constructor(options) {
        // When the new class, the parameter is passed to the constructor where options is El data computed...
        this.$el = options.el; // Create a current instance $el
        this.$data = options.data;
        // Determine if the root element exists <div id='app'></div>=>Compile template
        if (this.$el) {
            // Convert all data in the data to be defined with Object.defineProperty
            new Observer(this.$data);
            new Compiler(this.$el, this);
        }
    }
}
Note: What's wrong with this? 
In development, data can be acquired through instance + attribute (vm.a), while the MVVM we implemented acquires data through myMvm. $data.xxx, with an additional $data in between, which is obviously not what we wanted.Next, let the instance this proxy the $data data data, and myMvvm.xxx will do the same thing to get the data as the real scene.

Data Proxy

Add a method of property agent on the MVVM instance so that the property agent accessing myMvm is accessing the property of myMvm. $data.In fact, the Object.defineProperty() method was used to hijack the properties of the myMvvm instance object.The following proxy methods were added:

// this proxy $data
  for (let key in data) {
    Object.defineProperty(this, key, {
      enumerable: true,
      get() {
        return this.$data[key]; // this.xxx == {}
      },
      set(newVal) {
        this.$data[key] = newVal;
      }
    })
  }

Extend-Implement computed

computed has caching capability to update view changes when dependent attributes send changes

function initComputed() {
    let vm = this; // Mount the current this on the vm
    let computed = this.$options.computed;  // Get the computed property from options
    // All you get is the object's key, which can be converted into an array through Object.keys
    Object.keys(computed).forEach(key => {
        Object.defineProperty(vm, key, { // Map to this instance
            // Determine if the key in computed is an object or a function
            // If it is a function, call the get method directly
            // If it's an object, you need to manually invoke the get method
            // Because computed triggers only on dependent attributes, when dependent attributes are acquired, the system automatically calls the get method, so don't use Watcher to listen for changes
            get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
            set() {}
        });
    });
}

Project git address

https://github.com/tangmengcheng/mvvm.git Welcome your little buddies'comments and attention oh_star~

Related Issues

  • What are the drawbacks of Objest.defineProperty()?
  • Implement a listen on an array?
  • How is Proxy implemented in Vue3?
  • Rough implementation process for data two-way binding in Vue2.x source?

summary

Through the above description and demonstration of the core code, I believe that the small partners have a new understanding of MVVM, and the interviewer's questions can be answered smoothly.Hope your peers can tap once manually to implement a MVVM of their own, so as to have a deeper understanding of its principles.

With the increasing demand for monthly salary, the era of jQuery operating DOM can no longer meet the progress of rapid iteration of enterprise projects.MVVM mode is of great significance to the front-end field. Its core principle is to ensure real-time data synchronization between View layer and Model layer, and to achieve two-way data binding.Reduces frequent DOM operations, improves page rendering performance, and allows developers to spend more time processing data and developing business functions.

Last

All the students who wish to see the article have their rewards!

If there is something wrong with the article, you should correct it!

Finally, I wish you all more and more excellent!

Welcome to join us, learn the frontier and make progress together!

Published 9 original articles, won 21 reviews, and visited 288
Private letter follow

Tags: Fragment Vue Attribute Java

Posted on Thu, 13 Feb 2020 18:05:17 -0800 by eddjc