MVVM idea in Vue

1. what is MVVM

model-view-viewModel
model: Data model
View: view layer
viewModel: it can be understood as a bridge between view and model

His design idea is to pay attention to the changes of the model and automatically update the DOM state through viewModel, which is a major feature of Vue: data driven.

Image source: https://blog.csdn.net/CSDN_l512/article/details/90348847

2. What to do with MVVM in Vue

  1. Manipulate DOM through data
    We know that Vue uses concise template syntax to render data into the DOM system.
    For example: {{...}} to bind data, or use the v-html instruction to output HTML code, and so on.
    First of all, we should render these into the target content. The implementation ideas here are explained in detail in the mounted section.
  2. Automatically update DOM when data changes are detected
    When we find data changes, we can listen to the data changes, and then perform the first step to operate the DOM node to update the content. This involves what we often call the publish subscribe model. This part is explained in the beforeUpdate section at the end of the article.

3. discuss the implementation of MVVM in Vue based on Vue life cycle

Vue official website explains the Vue life cycle:

  • beforeCreate
    Vue creates a Vue instance object and uses this object to process DOM elements. At this time, the Vue object can be accessed.
    Using the beforeCreate hook, we can execute some methods before the object is initialized.
el:undefined  // The virtual DOM exists in the form of a virtual Dom and has not been mounted
data:undefined
  • created
    At this stage, all data and built-in methods are initialized, but el is still not mounted. At this time, data can be accessed.
    Of course, the initialization order of built-in methods is props = > methods = > data = > computed = > watch.
    If there is dynamic data to be requested, the request can be initiated at this stage.
  el:undefined  // The virtual DOM exists in the form of a virtual Dom and has not been mounted
  data:[object Object] // Initialized
  • beforeMouted

    Many things have been done in this step:
  1. First, judge whether there is an el object. If there is no el object, stop compiling, and the life cycle of the Vue instance ends at the end of create
  2. If there are attached DOM nodes, find out whether any template s are used in the DOM layer.
  3. If yes, put the template into the render function (if it is a single file component, the template will be compiled in advance).
  4. If there is no template, the external HTML will be compiled as a template (so we found that template takes precedence over the external HTML).
    Of course, in this process, if we use template syntax, such as {{...}} v-html, etc. they still exist in the form of virtual DOM and have not been compiled
el:[object HTMLDivElement] // Is mounted, but the template language has not yet been compiled
                           // For example: <div id= "app" > {{name}}</div>
data:[object Object] // Initialized
  • mounted

This step is to Compile the template language with the Compile module. Of course, this step will cause a lot of backflow and redrawing due to the replacement of the content, so this step is carried out in memory (document.createDocumentFragment()).

We have a general idea about content replacement:
Recursively traverse all nodes, including text nodes and element nodes.

/*  param All element nodes (this.el)
    Put nodes in memory */
node2fragment(node){
    // Create a document fragment in memory
    let fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild = node.firstChild){
        // Every time you get an element fragment, put it in memory
        // Because the node is put into memory, it can be thought of as an operation similar to stack out
        // Every time you get a new node, you will get it until the node is taken
        fragment.appendChild(firstChild);
    }
    return fragment
}

For text nodes: find out whether it contains {{}. If so, obtain the expression in {{}}, obtain the corresponding content of the expression, and render the content;

For element nodes: we need to find out whether there are V-related attributes. If there are, we need to obtain the instructions following v- and the corresponding values of the expression of the instructions;

/*  param   Memory node
    Compile DOM nodes in memory, replace {{}} contents with data, etc */
compile(fragment){
    // Get the child nodes of the first layer
    let childNodes = fragment.childNodes;
    [...childNodes].forEach( item =>{
        if( this.isElementNode(item) ){
            // If it is an element, find out if there is any instruction similar to v-model
            this.CompileElement(item);
            // If it is an element, recursively traverse the child nodes of the element
            this.compile(item);
        }else{
            // If it is text, see if there is {{}}
            this.CompileText(item);
        }
    })
}

For example:

<div id="app">
    {{name}}
    <input v-model="name"/>
</div>

<script>
    let vm = new Vue({
        el:"#app",
        data(){
            return{
                name:"Amy"
            }
        }
    })
</script>

We traverse the nodes, a div element, an input, and four text elements: a {{name} and three empty newlines
Then we judge them. The useful nodes are {{name}} and an input element, so we process them separately
For the text node {{}, we want to replace it with the text "Amy". For the input node, we want the attribute name and value bound.
After compilation, our DOM tree will be rendered to the page.

el:[object HTMLDivElement] // It has been mounted and the template language compilation is completed
                           // <div id="app">Amy</div>
data:[object Object] // Initialized

Supplementary browser rendering mechanism:

  1. Parsing HTML code to generate DOM tree
  2. Parsing CSS to generate CSSOM
  3. Combine DOM tree and CSSOM tree to generate render tree
  4. Using depth first traversal (diff algorithm) to traverse render nodes

Of course, the rendering order of css here is exactly the writing order. If the css writing order is not standardized, this step may also cause a lot of reflow and redrawing

  • beforeUpdate,updated

beforeUpdate is executed before listening for data changes. The virtual DOM is re rendered and the updates are applied. After the changes are completed, update is executed.

There is a problem here. How does Vue listen to data changes? How to notify the view layer to update?

This is the core idea of our MVVM. The view layer and data model are connected through the view model. Vue uses the publish subscribe mode, which we often call. Here, several classes are involved.

  1. Observer:
    Data hijacking to achieve two-way binding of data. Here, he is a publisher, publishing information about data changes.
    We use object The defineproperty () method recursively traverses the data objects that need to be observe d, including the properties of the child property objects, and binds getter and setter methods, so that you can listen to the changes of data read and modified each time.
const data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // Ha ha ha, the listening value has changed. King -- > DMQ

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // Get all attribute Traversals
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // Listening sub attribute
    Object.defineProperty(data, key, {
        enumerable: true, // enumerable 
        configurable: false, // No more define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('Hahaha, the monitoring value has changed ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}
  1. Dep:
    Collect the watcher dependency. A property has a Dep to notify the watcher of data changes.
    There is a sub array in the constructor to store multiple watchers, because an attribute may be used in multiple nodes, and each node using this attribute has a watcher.
    Generally, there are two methods: one is to subscribe to addSub (add Watcher) and the other is to publish notify (notify Watcher to update)
// And ellipsis
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // Listening sub attribute

    Object.defineProperty(data, key, {
        // And ellipsis
        set: function(newVal) {
            if (val === newVal) return;
            console.log('Hahaha, the monitoring value has changed ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // Notify all subscribers
        }
    });
}

function Dep() {
    this.subs = [];		// Store multiple watcher s
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
  1. Watcher:
    Subscriber. The Compiler adds a watcher for each compiled element node and text node. Once the data is updated, a watcher callback is triggered to notify the view layer of the change. var dep = new Dep(); It is defined inside the defineReactive method, so if you want to add subscribers through DEP, you must operate in the closure, so we can do it in the getter:
// Observer.js
// And ellipsis
Object.defineProperty(data, key, {
    get: function() {
        // Since it is necessary to add a watcher in the closure, define a global target attribute through Dep, temporarily store the watcher, and remove it after adding it
        Dep.target && dep.addSub(Dep.target);
        return val;
    }
    // And ellipsis
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // Here, the getter of the property will be triggered to add subscribers
        Dep.target = null;
    }
}

As a communication bridge between Observer and Compile, Watcher subscribers mainly do the following:

  1. Add yourself to the attribute subscriber (dep) during self instantiation
  2. Itself must have an update() method
  3. When the attribute change dep.notice() notification is given, you can call your own update() method and trigger the callback bound in Compile, and you will leave with success.
// Watcher.js
function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // In order to trigger the getter of the attribute, you can add yourself in the dep, which is easier to understand in combination with the Observer
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // Property value change notification received
    },
    run: function() {
        var value = this.get(); // Get the latest value
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // Execute the callback bound in Compile to update the view
        }
    },
    get: function() {
        Dep.target = this;    // Point the current subscriber to yourself
        var value = this.vm[exp];    // Trigger getter and add yourself to the attribute subscriber
        Dep.target = null;    // Add completed, reset
        return value;
    }
};
// The Observer and Dep are listed here again for easy understanding
Object.defineProperty(data, key, {
    get: function() {
        // Since it is necessary to add a watcher in the closure, you can define a global target attribute in Dep, temporarily store the watcher, and remove it after adding it
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // And ellipsis
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // Call the subscriber's update method to notify the change
        });
    }
};
  • beforeDestory
    Before Vue is destroyed and released from memory, all methods and instances can be accessed at this time.
    For example, we usually clear the timer at this stage.
  • destroyed
    When Vue instance memory is released, all sub components, practice listeners, and watcher s are cleared.

4. Data hijacking of vue2.0 and Vue3.0

Vue2.0 uses object Definepeoperty to hijack data; Vue3.0 uses Proxy data.
The biggest difference is object Definepeoperty can only Proxy a property of an object, but Proxy can directly Proxy objects and arrays.

Reference article:
https://juejin.im/post/5e492663f265da5709701728
https://segmentfault.com/a/1190000006599500

Tags: Vue mvvm

Posted by fabiuz on Mon, 30 May 2022 19:53:55 +0530