Deep understanding of Vue bidirectional data binding

**

MVVM

**
The two-way data binding of Vue refers to the two-way binding of model (model, that is, the data in Vue instance) and view (view), that is, one will change and the other will change.

First, let's understand what MVVM (model view viewmodel) is. In the MVVM architecture, the concept of ViewModel is introduced. ViewModel is equivalent to a middleware. ViewModel only cares about data and business processing, not how the View processes data. In this case, the View and Model can be independent. If either party changes, it does not necessarily need to change the other party. In addition, some reusable logic can be placed in one ViewModel to allow multiple views to reuse the ViewModel.

Taking the Vue framework as an example, ViewModel is an instance of a component. View is a template. Model can be completely separated from components when Vuex is introduced.

The whole process from change to rendering is roughly as follows:
First, data hijacking is used to monitor the data data changes. Once the data is monitored, the subscription starts to get the data, then the Dep notify method is sent to each subscription, and the subscription is rendered Dom based on the latest data.

-Three cores
The realization of bidirectional data binding is mainly based on the following three cores:
(1) observer listener
(2) watcher subscriber
(3) compile parser

Create a vue instance first:

const vm=new Vue({
    data() {
        return {
            msg: '111111',
            name: 'qwert',
            a: {
                b: 'bbbbb'
            }
        }
    },
    methods: {
        setName() {
            this.msg = '222';
        }
    },
    created() {
        this.msg = '212121';
        console.log('Instance initialization completed')
    },
    mounted() {
        console.log('DOM Mount complete')
    }
}).$mount('#app'); 

**

observer listener

**
There is a very important function object in observer Defineproperty (obj, prop, descriptor), which has three parameters:
• obj: the object on which the attribute is to be defined.
• prop: the name of the attribute to be defined or modified.
• descriptor: the attribute descriptor to be defined or modified
This method will directly define a new property on an object, or modify the existing property of an object and return the object.

We passed object Defineproperty () sets the hijacking of the data property and rewrites its get and set. Get is a function that is triggered when we read the value of the property. We usually put the watcher addition process into this function. Set is the trigger function that sets the value of the data property.

When the component is mounted, it will traverse all the attributes in the data, and then use object. For each attribute The defineproperty() function hijacks and corresponds to a new Dep() for each property. Dep is dedicated to collecting dependencies, deleting dependencies and sending messages to dependencies, that is, managing subscriber watchers. After all, a data can be used in multiple places. When the data changes, the corresponding different watchers need to be modified.

class Observer{
    constructor(data){
        this.$data = data;
        this.observer(this.$data);
    }
    observer(obj){
     // If data is not an object, an error will be prompted,
    // Because only objects can call object defineProperty
        if(typeof obj !== 'object') return;
        Object.keys(obj).forEach(key =>{
            this.defineReactive(obj, key, obj[key]);
            //Each property of the traversal object calls the defineReactive method for hijacking
        })
    }
    defineReactive(obj, key, value){
    // If the current value is still an object, recursive hijacking is used
        if(typeof value === 'object') this.observer(value);  
        let dep = new Dep();//Create a new subscriber to manage subscriber watcher s
        Object.defineProperty(obj, key, {
            get(){
                if(window.target){ 
      //The current Dep.target refers to the Watcher (subscriber) instance, which comes from the Watcher class below,
      //Here, the watcher is put into the subscriber dep for management, and the subscriber monitoring the data change is put into the subscriber array
                    dep.addSubs(); // Add Watcher instance to dep instance
                }
                return value;
            },
            set: (newVal) =>{
                if(value === newVal) return;
                // To prevent newVal from being an object, you need to change the properties in the object to be responsive again
                this.observer();//When the old and new values are different, call observe again to hijack the data
                value = newVal;//Set new value
                dep.notify();// The dep instance notifies the subscriber to make changes
       
            }
        })
    }
}

With object The set of defineproperty can realize the responsive binding from view to data, that is, it can obtain the data of page changes, then update the data to data, and then render it to the required place.

For dep, it has two main functions: 1 Add subscriber, 2 Notify subscribers to modify data

class Dep{
    constructor(){// Dep uniformly stores the functions to be executed in an array for management
        this.subs = [];
    }
    addSubs(){// Add a subscriber to the Dep instance. We call this method in the get method in observer
        this.subs.push(window.target);
    }
    notify(){// Notify the subscriber to make changes, and we call this method in set in observer.
      // Traverse all subscribers and call the update method on the subscriber to modify.
        this.subs.forEach(watcher => watcher.update());
    }
}

**

watcher subscriber

**
Watcher is a subscriber. It belongs to the bridge between Observer and Compile. It will receive the data changes generated by Observer, process the update message sent by Observer, and execute the update function bound by watcher according to the instructions provided by Compile to render the view, so as to change the data and promote the view change.

Watcher's first method is to update and render nodes. During the first rendering process, the Dep method will be automatically called to collect dependencies. After collection, each data in the component will be bound to the dependency. When the data changes, the corresponding dependency will be notified in seeter to update.

To read the data first during the update process, Wacther's second method will be triggered. Once triggered, the Dep method is automatically called again to collect dependencies. At the same time, patch (diff operation) is run in this function to update the corresponding DOM node, and the two-way binding is completed.

class Watcher{
    constructor(vm, expr, cb){
        this.$vm = vm;//vm is an object of new Vue in Vue
        this.expr = expr;  // Save the attribute to be modified, which can be the attribute value of the v-model or v-on: click instructions of the node node.
        this.cb = cb;// Callback function to be triggered when saving property modification
        this.getter(); // Save the initial value of the property and add the current subscriber (himself) to the subscriber dep
    }
    update(){//This method is called by dep.notify
        let newVal;
        if(typeof this.expr === 'function'){
            newVal = this.expr();
        } else {
            newVal = compileUtil.getValue(this.expr, this.$vm);
        }
        // let newVal = compileUtil.getValue(this.expr, this.$vm);
        if(this.value === newVal) return; 
        this.value = newVal;
        this.cb();
    }
    getter(){
        window.target = this;//Dep.target points to itself, that is, the watcher object, which is equivalent to putting the current watcher instance into the tartset
        if(typeof this.expr === 'function'){//Judge whether it is a function
            this.value = this.expr();
        } else {
            this.value = compileUtil.getValue(this.expr, this.$vm);// Gets the current property value
        }
        window.target = null;// Clear the current watcher on the static property of the Dep publisher
        //Because there is no target attribute in DEP, remember to release the unnecessary memory space after use. Dep.target = null;
    }
}

**

compile parser

**

The main function of Compile is to traverse all the child nodes of the bound dom node (i.e. the id bound by the el tag) and find all the v-instructions and "{}"

(1) If the child node contains a v-instruction, that is, an element node, a listening event is added to this element. (if it is v-on, then node.addEventListener('click '); if it is v-model, then node addEventListener(‘input’)). Then initialize the template element and create a Watcher binding element node.
(2) If the child node is a text node, i.e. "{data}}", the data in "{data}}" is taken out with a regular expression, and a new variable is used to replace the data in it.

class Compile{
    constructor(el, vm){
        this.$el = this.isElementNode(el) ? el: document.querySelector(el);
        this.$vm = vm;
        // Create the same element node as $el in memory
        let fragment = this.node2fragment(this.$el);
        // Resolve template ($el node)
        this.compile(fragment);
        // Remount the parsed node to the DOM tree
        this.$el.appendChild(fragment);
    }
    // Judge whether node is an element node
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // Determine whether it is a Vue instruction starting with v -
    isDirective(attr) {
        return attr.startsWith('v-');
    }
    isSpecialisDirective(attr){
        return attr.startsWith('@');
    }
    compile(fragment){ 
        // Gets the child node of the root node
        let childNodes  = fragment.childNodes;
        [...childNodes].forEach(child =>{
            if(this.isElementNode(child)){
                // Parse the attributes of the element node to see if there is a Vue instruction
                this.compileElement(child);
                // If the child node is also an element node, the function is executed recursively
                this.compile(child);
            }else{
                // Parse the text node to see if "{}}" exists
                this.compileText(child);
            }
        })
    }
    // Compile element
    compileElement(node){
        // Get all attributes of the element node
        let attrs = node.attributes;
        // Traverse all attributes to find out if there is a Vue instruction
        [...attrs].forEach(attr =>{ 
            // Name: attribute name, expr: attribute value
            let {name, value:expr} = attr; 
            // Judge whether it is an instruction
            if(this.isDirective(name)){
                let [,directive] = name.split('-');
                // If it is an instruction, set the response function of the node 
                compileUtil[directive](node, expr, this.$vm);
            }
            if(this.isSpecialisDirective(name)){
                let eventName = name.substr(1);
                compileUtil['on'](node, eventName, expr, this.$vm);
            }
        })
    }
    // Edit text
    compileText(node){
        let content = node.textContent;
        // Match {{xxx}}
        if(/\{\{(.+?)\}\}/.test(content)){
            compileUtil['contentText'](node, content, this.$vm);
        }
    }
    // Move node to memory
    node2fragment(node){
        // Create document fragment
        let fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = node.firstChild){
            // appendChild has mobility
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
}
const compileUtil = {
    getValue(expr, vm){
        let valOrFn = expr.split('.').reduce((totalValue, key) =>{
            if(!totalValue[key]) return null;
            return totalValue[key];
        }, vm)
        return typeof valOrFn === 'function' ? valOrFn.call(vm) : valOrFn;
    },
    setValue(expr, vm, value){
        return expr.split('.').reduce((totalValue, key, index, arr) =>{
            if(index === arr.length - 1) totalValue[key] = value;
            return totalValue[key];
        }, vm.$data)
    },
    getContentValue(content, vm){
        return content.replace(/\{\{(.+?)\}\}/g, (...args) =>{
            return this.getValue(args[1], vm); 
         })
    },
    contentText(node, content, vm){
        let fn = () =>{
            this.textUpdater(node, this.getContentValue(content, vm));
        }
        let resText = content.replace(/\{\{(.+?)\}\}/g, (...args) =>{
            // args[1] is xxx in {{xxx}}
            new Watcher(vm, args[1], fn);
            return this.getValue(args[1], vm);
        });
        // Directly replace text content by parsing for the first time
        this.textUpdater(node, resText);
    },
    text(node, expr, vm){
        let value = this.getValue(expr, vm);
        this.textUpdater(node, value);
        let fn = () =>this.textUpdater(node, this.getValue(expr, vm));
        new Watcher(vm, expr, fn);
    },
    textUpdater(node, value){
        node.textContent = value;
    },
    html(node, expr, vm){
        let value = this.getValue(expr, vm);
        this.htmlUpdater(node, value);
        let fn = () =>this.htmlUpdater(node, this.getValue(expr, vm));
        new Watcher(vm, expr, fn);
    },
    htmlUpdater(node, value){
        node.textContent = value;
    },
    model(node, expr, vm){
        let value = this.getValue(expr, vm);
        this.modelUpdater(node, value);
        let fn = () => this.modelUpdater(node, this.getValue(expr, vm));
        node.addEventListener('input', ()=>{
            this.setValue(expr, vm, node.value);
        })
        new Watcher(vm, expr, fn)
    },
    modelUpdater(node, value){
        node.value = value;
    },
    on(node, eventName, expr, vm){
        // Change this to a vm instance
        let fn = vm.$option.methods[expr].bind(vm);
        // Add event
        node.addEventListener(eventName, fn);
    }
}

Keywords: Javascript Vue

Added by clarencek on Mon, 24 Jan 2022 00:15:49 +0200