As a front-end interviewer I have to ask candidates: Describe your understanding of MVVM?

Next, I will realize a complete set of MVVM based on Vue from zero, which will be provided to the friends who are in the “Golden Three silver Four” job-hopping peak in the coming year to read and comb their own understanding of MVVM in detail.

What is the MVVM

Before we get to MVVM, let’s explain MVC. The MVC architecture was, and still is, on the back end. MVC respectively represents the 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 development of most parts of the business needs.

MVC & three-tier architecture

Taking Java as an example, the following describes the meanings and responsibilities of each layer in MVC and three-tier architecture:

  1. Model: The Model layer, which represents each JavaBean. It is divided into two categories, one is called data bearer beans, one is called business processing beans.
  2. View: The View layer, which represents the corresponding View page, interacts directly with the user.
  3. Controller: The control layer, which is the “middle man” between the Model and View, forwards user requests to the corresponding Model for processing, and processes the results of the Model to provide the corresponding response to the user.

This section uses login as an example to describe the logical relationship between the three layers. When the user clicks the login button on the View page, the system will retrieve the login interface in the Controller control layer. Generally, the Controller layer does not write much specific business logic code, but only an interface method, whose specific logic is implemented in the Service layer, and then the specific logic in the Service layer will call the Model Model in the DAO layer, so as to achieve dynamic effect.

The description of the MVVM

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

  1. M – Data Model (Model), simple JS objects
  2. Vm-viewmodel, which connects Model and View
  3. V-view layer (View), DOM rendering interface presented to the user

From the above MVVM template diagram, we can see that the most core is the ViewModel, which is mainly used: Monitor the DOM elements in View and bind the data in Model. When the View changes, the data in Modal will be changed, and the data in Model will trigger the View View to re-render, so as to achieve the effect of bidirectional data binding, which is also the most core feature of Vue.

Common libraries implement bidirectional data binding:

  • Publish and subscribe (backbone.js)
  • Dirty checking (angular.js)
  • Data hijacking (vue.js)

When asked about Vue’s two-way data binding principle, almost all interviewees will say: Vue adopts data hijacking combined with publish and subscribe mode. It uses Object.defineProperty() to hijack the getter and setter of each attribute, releases messages to subscribers when data changes, and triggers corresponding callback functions, so as to achieve two-way data binding. But when asked further:

  • What are the core modules needed to implement an MVVM?
  • Why do DOM manipulations happen in memory?
  • What is the relationship between the core modules?
  • How to do array data hijacking in Vue?
  • Have you ever implemented a full MVVM manually yourself?
  • .

Next, I will implement a complete set of MVVM step by step so that when asked mVVM-related questions again, I can stand out in the interview process. Before we start writing MVVM, it’s important to familiarize ourselves with the core API and publish and subscribe patterns:

Introduce the use of Object.defineProperty

Object.defineproperty (obj, prop, desc) defines a new property directly on an Object, or modifies an existing property

  1. Obj: The current object for which attributes need to be defined
  2. Prop: specifies the name of the property to be defined
  3. Desc: Property descriptor

Note: Normally, Object attributes can be modified or deleted by assigning values to them, but defining attributes with Object.defineProperty() allows more precise control of Object attributes by setting descriptors.

DefineProperty (obj, 'name', {64x: true, // default is false, writable: Enumerable: true, // Defaults to false, enumerable: true, // defaults to false, Set (val) {// set(val) {// set(val) {// set(val) {// set(val) {Copy the code

Note: When appear get,set, writable, enumerable, otherwise the system reported an error. The API does not support IE8 or later, that is, Vue is not compatible with IE8 or later browsers.

DocumentFragment – DocumentFragment

A DocumentFragment represents a DocumentFragment that does not belong to the DOM tree, but can store the DOM and add the stored DOM to the specified DOM node. So, some people say, well, why not just add elements to the DOM? The reason for using it is that manipulating the DOM with it is much more performance than manipulating the DOM directly.

Introduce the publish subscribe model

The publisher – subscriber pattern defines a one-to-many dependency where all dependent objects are notified and updated automatically when an object’s state changes, resolving the coupling of the functionality between the principal object and the observer. The following is a small example of a publish-subscribe pattern, which can be understood as an array relationship, where subscribe is put into a function and publish is executed by an array of functions.

Function Dep() {this.subs = []; function Dep() {this.subs = []; } // Subscribe dep.prototype. addSub = function(sub) {this.subs.push(sub); } Dep.prototype.notify = function() { this.subs.forEach(sub => sub.update()); } 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); Dep.addsub (watcher2); dep.notify(); // Console output: // 123 456Copy the code

Implement your own MVVM

To implement MVVM bidirectional binding, the following must be implemented:

  1. Implement a data hijacker – Observer that listens for all attributes of a data object and notifies subscribers of any changes to the latest values
  2. Implement a template compiler-compiler to scan and parse the instructions of each element node, replace the data according to the instruction template, and bind the corresponding update function
  3. Implement a -watcher that acts as a bridge between the Observer and Compile, subscribing to and receiving notification of each property change, and executing the corresponding callback function of the directive binding to update the view
  4. MVVM, as an entry function, integrates all three

Data hijacking – Observer

The primary purpose of the Observer class is to hijack all levels of data within the data data, giving it the ability to listen for changes in object properties

Key points:

  1. When the value of an object’s property is also an object, it is also hijack its value – recursion
  2. When the object is assigned the same value as the old value, no further operations are required – to prevent repeated rendering
  3. When a template rendering acquires an object property, it calls GET to add target, notifying the subscriber of an update – a data change, a view update
Constructor (data) {this. Observer (data); } observer(data) {if(data && typeof data == 'object') {for(let key in data) { this.defineReactive(data, key, data[key]); } } } defineReactive(obj, key, value) { let dep = new Dep(); this.observer(value); DefineProperty (obj, key, {get() {dep.target && dep.addSub(dep.target); return value; }, set (newVal) => {// set new value if(newVal! = value) {this.observer(newVal) {this.observer(newVal); // If the assignment is also to an object, observe value = newVal; dep.notify(); // Notify all subscribers of the update}}})}}Copy the code

Note: this class only does data hijacking on objects and does not listen on arrays.

Template Compiler – Compiler

Compiler is to parse the template instructions, replace the variables in the template with data, initialize the render page view, bind the corresponding node update function of each instruction, add subscribers to listen to the data, and update the view once the data changes, receive notification

Compiler does three main things:

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

Key points:

  1. Start by moving the actual DOM into memory – document fragmentation
  2. Compile element nodes and text nodes
  3. Add observers to expressions and attributes in the template
Class Compiler {/** * @param {*} el Document. getElementById('#app') * @param {*} VM instance */ constructor(el, Vm) {this.el = this.iselementNode (el) {this.el = this.iselementNode (el); el : document.querySelector(el); // console.log(this.el); Get the current template this.vm = vm; Let fragment = this.node2fragment(this.el); let fragment = this.node2fragment(this.el); // console.log(fragment); This.compile (fragment); this.compile(fragment); // 2. Replace the contents of the memory with this.el.appendChild(fragment); } /** * It contains the command * @param {*} attrName attribute name type v-modal */ isDirective(attrName) {return attrName.startsWith('v-'); // If v-} /** * compileElement node * @param {*} node element node */ compileElement(node) { 【 class array 】NamedNodeMap; NamedNodeMap{length: 0} let attributes = node.attributes; // Array.from(), [... XXX], [].slice.call, etc., can convert class arrays to real arrays [...attributes]. type="text" v-modal="obj.name" let {name, value: expr} = attr; If (this.isdirective (name)) {// v-modal v-html v-bind // console.log('element', node); Element let [, directive] = name.split('-'); // Different directives need to be called to handle CompilerUtil[directive](node, expr, this.vm); }}); {{}} * @param {*} node text node */ compileText(node) {let content = node.textContent; // console.log(content, 'content'); If (/\{\{(.+?)) // console.log(content, 'content'); // console.log(content, 'content'); // console.log(content, 'content'); Fetch {{}} child element CompilerUtil['text'](node, content, this.vm) {{}} child element CompilerUtil['text'](node, content, this.vm); }} /** * compile DOM nodes in memory * @param {*} fragmentNode document fragment */ compile(fragmentNode) { ChildNodes {{}} let childNodes = fragmentNode.childNodes; NodeLis [...childNodes]. ForEach (child => {// if (this.iselementNode (child)) { this.compileElement(child); This.compile (child); this.compile(); this.compile(); this.compile(); this.compile(); this.compile(); } else {// text node // console.log('text', child); this.compileText(child); }}); } @param {*} node node */ node2Fragment (node) {// Create a stable fragment; The purpose is to write every child of the node to the document fragments of the let fragments = document. CreateDocumentFragment (); let firstChild; While (firstChild = node.firstChild) {// appendChild has mobility, every time a node is moved into memory, AppendChild (firstChild) loses a node fragment.appendChild(firstChild); } return fragment; } @param {*} node {isElementNode(node) {return node.nodeType === 1; @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) }, /** * Handle V-modal * @param {*} node corresponding node * @param {*} expr expression * @param {*} VM current instance */ Modal (node, expr, Node. value = XXX let fn = this.updater['modalUpdater']; New Watcher(vm, expr, (newValue) => {new Watcher(vm, expr, (newValue) => { newValue) }) node.addEventListener('input', e => { let value = e.target.value; 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, (... New Watcher(vm, args[1], (newValue) => {fn(node, this.getContentValue(vm, expr)); }) return this.getVal(vm, args[1].trim()); }); fn(node, content); ModalUpdater (node, value) {node.value = value; }, // Handle the text node textUpdater(node, value) {node.textContent = value; }}}Copy the code

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

Publish subscriptions – Watcher

Watcher subscribers, acting as a bridge between the Observer and Compile, mainly do the following:

  1. Add yourself to the attribute subscriber (DEP) during self instantiation
  2. There must be an update() method itself
  3. If you can call your own update() method and trigger the callback bound with Compile when notified of property changes to dep.notice(), you are done.
Function Dep() {this.subs = []} Dep. Prototype = function(sub) {this.subs.push(sub)} Prototype. Notify = function() {this.subs.foreach (sub => sub.update())} /** * watcher * @param {*} vm * @param */ 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; Function () {let val = this.vm; let arr = this.exp.split('.'); arr.forEach(function (k) { val = val[k]; }) this.fn(val) }Copy the code

Dep and Watcher are implementations of the simple observer pattern. Dep is the subscriber, which manages all observers and has the ability to send messages to observers. Watcher is the Watcher. After receiving the subscriber’s message, the Watcher will make its own update.

Integration – MVVM

MVVM, as the entry of data binding, integrates Observer, Compile and Watcher, uses Observer to monitor its model data changes, and uses Compile to parse and Compile template instructions. Finally, Watcher is used to build a communication bridge between Observer and Compile to achieve data change -> view update; View Interactive Changes (INPUT) -> Bidirectional binding effect of data model changes.

Class MVVM {constructor(options) {// When this class is new, the parameters are passed to the constructor. Options is el Data computed... this.$el = options.el; $el this.$data = options.data; <div id='app'></div> => Compile the template if (this.$el) {// Convert all the data in the data to object.defineProperty to define new Observer(this.$data); new Compiler(this.$el, this); }}}Copy the code
Note: There is a problem with this? $data. XXX = myMvvm.$data. XXX = myMvvm. $data = myMvvm. XXX = myMvvm. XXX = myMvvm.Copy the code

Data brokers

Add a property broker method on the MVVM instance so that the property broker accessing myMvvm is the property accessing myMvvm.$data. The object.defineProperty () method is used to hijack the properties of myMvvm instance objects. Add the following proxy methods:

$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; }})}Copy the code

Extension – Implements computed

Computed has caching capabilities that update view changes when dependent properties send changes

function initComputed() { let vm = this; // Mount the current this to the VM let computed = this.codeoptions.com puted; Keys can be converted to an array object.keys (computed). ForEach (key => { Object.defineproperty (vm, key, {// Map to this instance // Determine whether a computed key is an Object or a function // If a function, call the get method directly // If an Object, // Because computed is triggered only by dependent attributes, the system automatically calls the GET method when a dependency attribute is obtained, so don't use Watcher to listen for changes in get: typeof computed[key] === 'function' ? computed[key] : computed[key].get, set() {} }); }); }Copy the code

Project Git address

https://github.com/tangmengcheng/mvvm.git welcome friend thumb up, comments and concerns oh 🤭 star ~Copy the code

Issues related to

  • What are the drawbacks of objest.defineProperty ()?
  • Implement a listener on an array?
  • How is Vue3 implemented with Proxy?
  • Vue2. X source data bidirectional binding roughly implementation process?

conclusion

Through the above description and the demonstration of the core code, I believe that partners have a new understanding of MVVM, and can answer the questions of the interviewer in the interview smoothly. Hope that the peer partner manually knock again, to achieve a MVVM of their own, so that the principle of understanding more in-depth.

With the increasing demands of monthly salary, the era of jQuery DOM manipulation is no longer enough for the rapid iteration of enterprise projects. MVVM mode is of great significance to the front-end field. Its core principle is to ensure data synchronization between View layer and Model layer in real time and realize two-way data binding. This reduces frequent DOM manipulation, improves page rendering performance, and allows developers to spend more time on data processing and business functionality development.

The last

If this article has helped you, give it a thumbs up ❤️❤️❤️

Welcome to join us to learn the front end and make progress together!