I believe that as long as you go to interview vUE, will be asked about vUE two-way data binding, if you say MVVM is a view model model view, as long as the data changes the view will also be updated! Then you are not far away from being passed!

Video has been recorded, address (www.bilibili.com/video/BV1qJ…)

Several ways to achieve bidirectional binding

At present, several mainstream MVC (VM) frameworks have implemented one-way data binding, and the two-way data binding I understand is nothing more than to add change(input) events to input elements (input, Textare, etc.) on the basis of one-way binding to dynamically modify the model and view, without much depth. So don’t worry too much about the unidirectional or bidirectional bindings that are implemented.

There are several ways to implement data binding:

Publish-subscriber mode (backbone.js)

Dirty value checking (Angular.js)

Data hijacking (vue.js)

Set (‘property’, value) is used to update the data. The default value is the value (‘property’, value). The default value is the value (‘property’)

This is now too low. We would rather update the data using the vm.property = value method and automatically update the view, so there are two ways to do this

Dirty value check: Angular. Js uses dirty value detection to determine whether or not to update the view by comparing data changes. The simplest way to detect changes is to periodically poll data with setInterval(). Something like this:

  • DOM events, such as the user entering text, clicking a button, etc. ( ng-click )
  • XHR response event ($HTTP)
  • Browser Location change event ($Location)
  • The Timer event (interval )
  • performapply()

Data hijacking: Vue.js hijacks the setters and getters of each property through object.defineProperty () in the mode of data hijacking combined with publist-subscriber mode, publishes messages to subscribers when data changes and triggers the corresponding listening callback.

Principle of MVVM

The core method of Vue response principle is to hijack attributes through object.defineProperty () to achieve the purpose of monitoring data changes. Undoubtedly, this method is one of the most important and basic contents in this paper

In order to achieve MVVM bidirectional binding, you must implement the following:

  • 1. Implement a data listener Observer, which can monitor all properties of data objects and get the latest value if there is a change and notify subscribers
  • 2. Implement a instruction parser Compile, scan and parse the instructions of each element node, replace 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 attribute change, executing the corresponding callback function for the instruction binding, and updating the view
  • 4, MVVM entry function, integration of the above three

Let’s look at the previous vue features


      
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <h2>{{obj.name}}--{{obj.age}}</h2>
        <h2>{{obj.age}}</h2>
        <h3 v-text='obj.name'></h3>
        <h4 v-text='msg'></h4>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-html='htmlStr'></div>
        <div v-html='obj.fav'></div>
        <input type="text" v-model='msg'>
        <img v-bind:src="imgSrc" v-bind:alt="altTitle">
        <button v-on:click='handlerClick'>Button 1</button>
        <button v-on:click='handlerClick2'>Button 2</button>
        <button @click='handlerClick2'>Button 3</button>
    </div>
    <script src="./vue.js"></script>
    <script>
        let vm = new MVue({
            el: '#app'.data: {
                obj: {
                    name: 'Little Horse'.age: 19.fav:'< h4 > front-end Vue < / h4 >'
                },
                msg: 'How MVVM is implemented'.htmlStr:"<h3>hello MVVM</h3>".imgSrc:'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1568782284688&di=8635d17d550631caabfeb4306b5d76fa&i mgtype=0&src=http%3A%2F%2Fh.hiphotos.baidu.com%2Fimage%2Fpic%2Fitem%2Fb3b7d0a20cf431ad7427dfad4136acaf2fdd98a9.jpg'.altTitle:'eyes'.isActive:'true'

            },
            methods: {
                handlerClick() {
                    alert(1);
                    console.log(this);
                    
                },
                handlerClick2(){
                    console.log(this);
                    alert(2)}}})</script>
</body>

</html>
Copy the code

Implement the instruction parser Compile

Implement a directive parser Compile, scan and parse the instructions of each element node, replace data according to the directive template, bind the corresponding update function, add subscribers who listen to data, and once the data changes, receive notification and update the view, as shown in the figure:

Initialize the

New MVue. Js

class MVue {
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        // Save the options parameter, which will be used later to process the data
        this.$options = options;
        // Compile the template if the root element exists
        if (this.$el) {
            // 1. Compile an instruction parser
            new Compile(this.$el, this)}}}class Compile{
    constructor(el,vm) {
        // Determine if the el parameter is an element node, if it is directly assigned, and if it is not, get the assignment
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
    }
    isElementNode(node){
        // Check whether it is an element node
        return node.nodeType === 1}}Copy the code

So the outside world can do this

let vm = new Vue({
    el:'#app'
})
//or
let vm = new Vue({
    el:document.getElementById('app')})Copy the code

Optimize compilation using document fragmentation

<h2>{{obj.name}}--{{obj.age}}</h2>
<h2>{{obj.age}}</h2>
<h3 v-text='obj.name'></h3>
<h4 v-text='msg'></h4>
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-html='htmlStr'></div>
<div v-html='obj.fav'></div>
<input type="text" v-model='msg'>
<img v-bind:src="imgSrc" v-bind:alt="altTitle">
<button v-on:click='handlerClick'>Button 1</button>
<button v-on:click='handlerClick2'>Button 2</button>
<button @click='handlerClick2'>Button 3</button>
Copy the code

Next, find the value of the child element, such as obj.name,obj.age,obj.fav, find obj, find fav, get the value in the data and replace it

However, we have to think of a problem here. Every time we find a data replacement, we have to re-render it, which may cause backflow and redrawing of the page. Therefore, the best way is to put the above elements in memory and replace them after the operation is completed in memory.

class Compile {
    constructor(el, vm) {
        // Determine if the el parameter is an element node, if it is directly assigned, and if it is not, get the assignment
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // Because every time a match is made, the page will backflow and redraw, affecting the performance of the page
        // So you need to create document fragments for caching, reducing page reflux and redrawing
        // 1. Obtain the document fragmentation object
        const fragment = this.node2Fragment(this.el);
        // console.log(fragment);
        // 2. Compile the template
        // 3. Add all the contents of the child elements to the root element
        this.el.appendChild(fragment);

    }
    node2Fragment(el) {
        const fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment
    }
    isElementNode(el) {
        return el.nodeType === 1; }}Copy the code

At this time, you will find that the page has not changed any before, but through the Fragment processing, optimize the page rendering performance

Compiling templates

// The class to compile the data
class Compile {
    constructor(el, vm) {
        // Determine if the el parameter is an element node, if it is directly assigned, and if it is not, get the assignment
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // Because every time a match is made, the page will backflow and redraw, affecting the performance of the page
        // So you need to create document fragments for caching, reducing page reflux and redrawing
        // 1. Obtain the document fragmentation object
        const fragment = this.node2Fragment(this.el);
        // console.log(fragment);
        // 2. Compile the template
        this.compile(fragment)

        // 3. Add all the contents of the child elements to the root element
        this.el.appendChild(fragment);

    }
    compile(fragment) {
        // 1. Obtain the child node
        const childNodes = fragment.childNodes;
        // 2. Iterate over the child nodes
        [...childNodes].forEach(child= > {

            // 3. Handle different types of child nodes
            if (this.isElementNode(child)) {
                // is an element node
                // Compile the element nodes
                // console.log(' I am the element node ',child);
                this.compileElement(child);
            } else {
                // console.log(' I am a text node ',child);
                this.compileText(child);
                // All that is left is the text node
                // Compile the text node
            }
            // 4. Remember to iterate over the child elements recursively
            if (child.childNodes && child.childNodes.length) {
                this.compile(child); }})}// Compile the text method
    compileText(node) {
        console.log('Compile text')

    }
    node2Fragment(el) {
        const fragment = document.createDocumentFragment();
        // console.log(el.firstChild);
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment
    }
    isElementNode(el) {
        return el.nodeType === 1; }}Copy the code

Next, render according to the type of the different child elements

Compile the element

compileElement(node) {
    // Get all properties of this node
    const attributes = node.attributes;
    // Iterate over the attributes
    [...attributes].forEach(attr= > {
        const { name, value } = attr; //v-text v-model v-on:click @click
        // See if the current name is a directive
        if (this.isDirective(name)) {
            // Perform an operation on v-text
            const [, directive] = name.split(The '-'); //text model html
            // v-bind:src
            const [dirName, eventName] = directive.split(':'); // Handle v-ON :click
            // Update the data
            compileUtil[dirName] && compileUtil[dirName](node, value, this.vm, eventName);
            // Remove attributes from the current element
            node.removeAttribute('v-' + directive);

        }else if(this.isEventName(name)){
            // Handles the event in this case @click
            let [,eventName] =  name.split(The '@');
            compileUtil['on'](node, value, this.vm, eventName)
        }

    })

}
// If @click is the name of the event
isEventName(attrName){
    return attrName.startsWith(The '@')}// Check whether it is an instruction
isDirective(attrName) {
    return attrName.startsWith('v-')}Copy the code

Compile the text

// Compile the text method
compileText(node) {
    const content = node.textContent;
    // Match the contents of {{XXX}}
    if (/ \ {\ {(. +?) \} \} /.test(content)) {
        // Process text nodes
        compileUtil['text'](node, content, this.vm)
    }

}
Copy the code

You’ll also find out about compileUtil what the hell is that? The actual compilation operation I’m going to put into this object, and I’m going to do different things depending on the instructions. For example, V-text is for text, V-HTML is for HTML elements, and V-Model is for form data…..

So we initialize the view with the updater function in the current object compileUtil

Process elements/Process text/Process events….

const compileUtil = {
    // The method to get the value
    getVal(expr, vm) {
        return expr.split('. ').reduce((data, currentVal) = > {
            return data[currentVal]
        }, vm.$data)
    },
    getAttrs(expr,vm){

    },
    text(node, expr, vm) { //expr might be {{object.name}}--{{object.age}}
        let val;
        if (expr.indexOf('{{')! = =- 1) {
            // 
            val = expr.replace(/ \ {\ {(. +?) \}\}/g, (... args) => {return this.getVal(args[1], vm); })}else{ V -text='obj. Name 'v-text=' MSG'
            val = this.getVal(expr,vm);
        }
        this.updater.textUpdater(node, val);
    },
    html(node, expr, vm) {
        // HTML processing is very simple. You can just call the update function
        let val = this.getVal(expr,vm);
        this.updater.htmlUpdater(node,val);
    },
    model(node, expr, vm) {
        const val = this.getVal(expr,vm);
        this.updater.modelUpdater(node,val);
    },
    // Handle the event
    on(node, expr, vm, eventName) {
        // Get the event function
        let fn = vm.$options.methods && vm.$options.methods[expr];
        // Add the event because we don't need to worry about this pointing when using vue, because the source code internally handles this pointing for us
        node.addEventListener(eventName,fn.bind(vm),false);
    },
    // Bind properties The simple properties are already handled with the class name and the style of binding is a little bit more complicated because the corresponding value could be an object or an array and you can try to write it according to your ability
    bind(node,expr,vm,attrName){
        let attrVal = this.getVal(expr,vm);
        this.updater.attrUpdater(node,attrName,attrVal);
    },
    updater: { attrUpdater(node, attrName, attrVal){ node.setAttribute(attrName,attrVal); }, modelUpdater(node,value){ node.value = value; }, textUpdater(node, value) { node.textContent = value; }, htmlUpdater(node,value){ node.innerHTML = value; }}}Copy the code

This is done: we implement a compiler that parses instructions and initializes views with an updater

Implement a data listener Observer

Let’s do it we know that we can use obeject.defineProperty () to listen for property changes so we recurse through the data object that needs to observe, including the properties of the child property object, You put a setter and a getter on it so that if you assign a value to this object, it triggers the setter, so you can listen for changes. The relevant code could look like this:

//test.js
let data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; Kindeng --> DMQ --> DMQ

function observe(data) {
    if(! data ||typeofdata ! = ='object') {
        return;
    }
    // Fetch all attributes to iterate
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // Listen for child attributes
    Object.defineProperty(data, key, {
        enumerable: true./ / can be enumerated
        configurable: false.// No more define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('Hahaha, I'm listening for a change.', val, '- >', newVal); val = newVal; }}); }Copy the code

Looking at the diagram, what we are going to implement next is a data listener Observer that listens for all properties of the data object, notifishes the dependent collection object (Dep) of changes, and notifishes the subscriber (Watcher) to update the view

// Create a data listener to hijack and listen for all data changes
class Observer{
    constructor(data) {
        this.observe(data);
    }
    observe(data){
        // Hijack and listen if the current data is an object
        if(data && typeof data === 'object') {// Iterate over the properties of the object to listen
            Object.keys(data).forEach(key= >{
                this.defineReactive(data,key,data[key]);
            })
            
        }
    }
    defineReactive(obj,key,value){
        // Loop recursively to look at all levels of data
        this.observe(value);// So obj can also be observed
        Object.defineProperty(obj,key,{
            get(){
                return value;
            },
            set:(newVal) = >{
                if(newVal ! == value){// If the object is directly modified by the outside world, the new value is observed again
                    this.observe(newVal);
                    value = newVal;
                    // Notify changesdep.notify(); }}}Copy the code

So that we can monitor the change of each data, then after listening to change is how to notify the subscriber, so we need to implement a message subscriber, is very simple, maintain an array, used to gather the subscriber, trigger notify data changes, then call the subscriber the update method, code after improvement is this:

Create a Dep

  • Add a subscriber
  • Define the method of advice
class Dep{
    constructor() {
        this.subs = []
    }
    // Add subscribers
    addSub(watcher){
        this.subs.push(watcher);
 
    }
    // Notify changes
    notify(){
        // The observer has an update method to update the view
        this.subs.forEach(w= >w.update()); }}Copy the code

Although we have created the Observer, the Dep(subscriber), then the question arises, who are the subscribers? How do I add a subscriber to a subscriber?

Const dep = new dep (); Is defined within defineReactive, so to add subscribers via dep, you have to do it inside the closure, so we can do it inside getOldVal:

Implement a Watcher

It acts as a bridge between the Observer and Compile, subscribes to and receives notification of each attribute change, executes the corresponding callback function for the instruction binding, and updates the view

As long as the thing is done:

1. Add yourself to the property subscriber (DEP) when it instantiates itself

2. You must have an update() method of your own

3. When dep.notify() is notified of property changes, you can call your update() method and trigger the callback bound in Compile, then you are done.

//Watcher.js
class Watcher{
    constructor(vm,expr,cb) {
        // Watch for changes in the new and old values and update the view if there are changes
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // Save the old value first
        this.oldVal = this.getOldVal();
    }
    getOldVal(){
        Dep.target = this;
        let oldVal = compileUtil.getVal(this.expr,this.vm);
        Dep.target = null;
        return oldVal;
    }
    update(){
        // Dep will notify the observer to update the view when the data changes during the update operation
        let newVal = compileUtil.getVal(this.expr, this.vm);
        if(newVal ! = =this.oldVal){
            this.cb(newVal); }}}//Observer.js
defineReactive(obj,key,value){
    // Loop recursively to look at all levels of data
    this.observe(value);// So obj can also be observed
    const dep = new Dep();
    Object.defineProperty(obj,key,{
        get(){
            // Subscribe to the data change and add observers to the Dep
            Dep.target && dep.addSub(Dep.target);
            return value;
        },
        / /... omit})}Copy the code

When we modify some data, the data has changed, but the view has not been updated

When should we add the binding watcher

That is, we bind the update function to the watcher to update the view when the subscribed data changes

Modify the

// Compile the template tool class
const compileUtil = {
    // The method to get the value
    getVal(expr, vm) {
        return expr.split('. ').reduce((data, currentVal) = > {
            return data[currentVal]
        }, vm.$data)
    },
    / / set the value
    setVal(vm,expr,val){
        return expr.split('. ').reduce((data, currentVal, index, arr) = > {
            return data[currentVal] = val
        }, vm.$data)
    },
    // Get new values for {{a}}--{{b}}
    getContentVal(expr, vm) {
        return expr.replace(/ \ {\ {(. +?) \}\}/g, (... args) => {return this.getVal(args[1], vm);
        })
    },
    text(node, expr, vm) { //expr might be {{object.name}}--{{object.age}}
        let val;
        if (expr.indexOf('{{')! = =- 1) {
            // 
            val = expr.replace(/ \ {\ {(. +?) \}\}/g, (... args) => {// Bind watcher to update the view
                new Watcher(vm,args[1], () = > {this.updater.textUpdater(node,this.getContentVal(expr, vm));
                })
                return this.getVal(args[1], vm); })}else{ V -text='obj. Name 'v-text=' MSG'
            val = this.getVal(expr,vm);
        }
        this.updater.textUpdater(node, val);

    },
    html(node, expr, vm) {
        // HTML processing is very simple. You can just call the update function
        let val = this.getVal(expr,vm);
        // Bind watcher when the subscription data changes, thus updating the function
        new Watcher(vm,expr,(newVal)=>{
            this.updater.htmlUpdater(node, newVal);
        })
        this.updater.htmlUpdater(node,val);
    },
    model(node, expr, vm) {
        const val = this.getVal(expr,vm);
        // Bind the update function to update views when data changes

        // Data ==> View
        new Watcher(vm, expr, (newVal) => {
            this.updater.modelUpdater(node, newVal);
        })
        View ==> Data
        node.addEventListener('input',(e)=>{
            / / set the value
            this.setVal(vm,expr,e.target.value);

        },false);
        this.updater.modelUpdater(node,val);
    },
    // Handle the event
    on(node, expr, vm, eventName) {
        // Get the event function
        let fn = vm.$options.methods && vm.$options.methods[expr];
        // Add the event because we don't need to worry about this pointing when using vue, because the source code internally handles this pointing for us
        node.addEventListener(eventName,fn.bind(vm),false);
    },
    // Bind properties The simple properties are already handled with the class name and the style of binding is a little bit more complicated because the corresponding value could be an object or an array and you can try to write it according to your ability
    bind(node,expr,vm,attrName){
        let attrVal = this.getVal(expr,vm);
        this.updater.attrUpdater(node,attrName,attrVal);
    },
    updater: { attrUpdater(node, attrName, attrVal){ node.setAttribute(attrName,attrVal); }, modelUpdater(node,value){ node.value = value; }, textUpdater(node, value) { node.textContent = value; }, htmlUpdater(node,value){ node.innerHTML = value; }}}Copy the code

The proxy agent

When using vue, we can usually get data directly from vm. MSG, because the vue source code internally does a layer of proxy. In other words, the value operation on the DATA acquisition vm is represented on the VM.$data

class Vue {
    constructor(options) {
        this.$data = options.data;
        this.$el = options.el;
        this.$options = options
        // Compile the template if the root element exists
        if (this.$el) {
            // 1. Implement a data listener Observe
            // Can listen to all attributes of the data object, if there is a change to get the latest value and notify subscribers
            // object.definerProperty ()
            new Observer(this.$data);

            $data = $data = $data = $data
            this.proxyData(this.$data);
            
            // 2. Implement an instruction parser, Compile
            new Compiler(this.$el, this); }}// Act as an agent
    proxyData(data){
       for (const key in data) {
          Object.defineProperty(this,key,{
              get(){
                  returndata[key]; }, set(newVal){ data[key] = newVal; }}}Copy the code

The interview questions

Explain your understanding of vUE’s MVVM responsive principle

Vue.js hijacks the setters and getters of each property through object.defineProperty () in the mode of data hijacking combined with publist-subscriber mode, publishes messages to subscribers when data changes and triggers the corresponding listening callback.

As the entry point of data binding, MVVM integrates Observer, Compile and Watcher. It listens to its own model data changes through Observer and parses the compilation template instructions through Compile. Finally, use Watcher 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

With that picture above, it’s hard not to get hired