Principle of Bidirectional Binding of vue Data

Keywords: Attribute Fragment Vue

1. Implementation principle of vue

Vue.js uses the attribute feature functionality provided by ES5, combined with the publish-subscribe mode, to publish messages to subscribers and trigger corresponding listening callbacks when data changes through Object.defineOrototype() to define set and get attribute methods.

The steps are as follows:

  1. Recursively traverse data objects that need to be observed, including properties of child property objects, set and get attribute methods.When a value of this object is assigned, the bound set attribute method is triggered so that you can listen for data changes.
  2. Parse the template instructions with complie, replace the variables in the template with data, initialize the rendered page view, bind the nodes corresponding to each instruction to update functions, and add subscribers that listen to the data.As soon as the data changes, you are notified and the view is updated.
  3. Watcher subscribers are bridges between Observer publishers and complie parsing template directives with the following main functions:
  • Add yourself when instantiating yourself, like in a property subscriber (Dep);
  • It must have an update() method of its own;
  • When dep.notice() publishes a notification, it can call its own update() method and trigger a callback function bound in complie.
  1. MVVM is the entrance to data binding. It integrates Watcher, Observer, Complie, monitors the changes of your model data through Observer, parses template instructions through Complie, and finally uses Watcher to bridge the communication between Observer and Compplie to achieve the effect of data change notification view update.Change updates the bidirectional binding effect of data model changes using view interaction.

[Supplementary]:

  1. Specific use of Object.defineProperty(): https://mp.csdn.net/mdeditor/99162136#

  2. Subscriber and Publisher Modes:

(1) The so-called subscribers are like subscribing to a newspaper in our daily life.When subscribing to a newspaper, we usually have to register with a newspaper or some intermediary.When a new version of a newspaper is published, the postman needs to distribute it to the subscribers in turn.

(2) There are two steps to implement this pattern with code:

  • Initialize publisher, subscriber.
  • Subscribers need to be registered with the publisher, who in turn publishes messages to subscribers when they publish them.

2. Implement a simple mvvm bidirectional binding demo through vue principle:

  1. Idea Analysis
  • To implement mvvm, there are two main aspects, view change updates data and data change updates view.

  • View change update data can actually be achieved through event monitoring, such as input tag listening for input events, all of which we focus on analyzing data change update view.

  • Data Changes Update View focuses on how to know when the view has changed, and as long as you know when the view has changed, the next step will be handled.Setting a set function on a property through Object.defineProperty() triggers the function when the property changes, so we just need to put some updated methods in the set function to update the view of the data change.

  1. Implementation process
  • The first step is to listen for data hijacking, so we first set up a listener Observer to listen for all attributes, and when the attributes change, we need to notify the subscriber Watcher to see if updates are needed.

  • Since attributes can be multiple, so there will be multiple subscribers, we need a message subscriber Dep to specifically collect these subscribers and to unify management between the listener Observer and the subscriber Watcher.Since there may be some directives on the node elements, we also need an instruction parser Compile that scans and parses each node element, initializes the directives into a subscriber Watcher, replaces the template data, and binds the corresponding functions, when the subscriber Watcher accepts changes in the corresponding properties, it executes the corresponding update functionsTo update the view.

  1. To organize the above ideas, we need to implement three steps to complete the two-way binding:

(1) Implement a listener Observer that hijacks and listens for all attributes and notifies subscribers of any changes.

(2) Implement a subscriber Watcher that receives notifications of property changes and executes functions to update the view.

(3) Implement a parser Compile that scans and parses instructions for each node and initializes the corresponding subscriber based on the initialization template data.

  1. Realization

(1) Implement a listener Observer

The core method of the data listener is Object.defineProperty(), which listens on all attribute values through a traversal loop and processes them with Object.defineProperty(), code as follows:

//Listen on all attributes, iterate through them recursively
function defineReactive(data,key,val) {
    observe(val);  //Recursively traverse all attributes
    Object.defineProperty(data,key,{
        enumerable:true,    
        configurable:true,   
        get:function() {
            return val;
        },
        set:function(newVal) {
            val = newVal;
            console.log('attribute'+key+'Already monitored,The present value is:"'+newVal.toString()+'"');
        }
    })
}
function observe(data) {
    if(!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key){
        defineReactive(data,key,data[key]);
    });
}
var library = {
    book1: {
        name: ''
    },
    book2: ''
};
observe(library);
library.book1.name = 'vue Authoritative Guide';  
library.book2 = 'No such book';   
  • All attributes are traversed down through the observe() method and data hijacking listens through the defineReactive() method.

  • In the above idea, we need a message subscriber Dep that can hold message subscribers. Message subscriber Dep mainly collects message subscribers, and then executes the update function for the corresponding subscribers when the properties change. Dep needs a container to hold message subscribers.

Modify the monitor Observer above:

function defineReactive(data,key,val) {
    observe(val);
    var dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if (Do you need to add subscribers) {    //Watcher Initialization Trigger
                dep.addSub(watcher);  //Add a Subscriber
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log('attribute' + key + 'Has been listened on, now the value is: "' + newVal.toString() + '"');
            dep.notify(); // Notify all subscribers if data changes
        }
    });
}
function observe(data) {
    if(!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key){
        defineReactive(data,key,data[key]);
    });
}
function Dep() {
    this.subs = [];
}
Dep.prototype = {                        
    addSub:function(sub) {
        this.subs.push(sub);
    },
    notify:function() {
        this.subs.forEach(function(sub) {
            sub.update();  //Notify each subscriber to check for updates
        })
    }
}
Dep.target = null;
  • Adding Subscriber Dep to a Subscriber is designed in the get to have Watcher trigger at initialization, so determine if you need to add Subscribers.

  • In the set method, if the function changes, all subscribers are notified and the subscriber executes the corresponding update function.

  • [Supplement] Usage of Object.keys(): https://mp.csdn.net/mdeditor/99232258#

(2) Implement Subscriber Watcher

  • Subscriber Watcher initially adds itself to Subscriber Dep, so how?

The listener Observer performs the add subscriber operation in the get function, so you only need to trigger the corresponding get function when the subscriber Watcher initializes to perform the add subscriber operation.

  • How do I trigger the corresponding get function?
    Getting the corresponding property value triggers the corresponding get through Object.defineProperty().

  • Note that adding subscribers only needs to be performed when the subscriber is initialized, so we need to make a judgment that the subscriber is cached on Dep.target and removed after successful addition, as follows:

function Watcher(vm,exp,cb) {
    this.vm = vm;    //Scope pointing to SelfVue
    this.exp = exp;  //The key value of the bound property
    this.cb = cb;    //closure
    this.value = this.get();
}
Watcher.prototype = {
    update:function() {
        this.run();
    },
    run:function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if(value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm,value,oldVal);
        }
    },
    get:function() {
        Dep.target = this;                   // Cache yourself
        var value = this.vm.data[this.exp];  // Enforce get functions in listeners
        Dep.target = null;                   // Release yourself
        return value;
    }
}

At this point we need to make a slight adjustment to defineReactive() in Observer:

function defineReactive(data,key,val) {
    observe(val);
    var dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if(Dep.target) {   //Determine if subscribers need to be added
                 dep.addSub(Dep.target);
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log('attribute' + key + 'Has been listened on, now the value is: "' + newVal.toString() + '"');
            dep.notify(); // Notify all subscribers if data changes
        }
    });
}

So far, a simple version of Watcher has been formed, and we can achieve a simple two-way binding simply by associating the subscriber Watcher with the listener Observer. Since there is no instruction parser designed here, we all write-to-death the template data, assuming there is a node element on the template with id'name'and a two-way bindingThe bound variable is also'name', wrapped in two brackets (which is not useful for the moment), and the template code is as follows:

<body>
    <h1 id="name">{{name}}</h1>
</body>

We need to define a SelfVue class to associate the observer with the watcher in the following code:

//Associate Observer with Watcher
function SelfVue(data,el,exp) {
    this.data = data;
    observe(data);
    el.innerHTML = this.data[exp];
    new Watcher(this,exp,function(value) {
        el.innerHTML = value;
    });
    return this;
}

Then a new SelfVue on the page can bind in both directions:

<body>
    <h1 id="name"{{name}}></h1>
</body>
<script src="../js/observer.js"></script>
<script src="../js/Watcher.js"></script>
<script src="../js/SelfVue.js"></script>
<script>
     var ele = document.querySelector('#name');
     var selfVue = new SelfVue({
         name:'hello world'
     },ele,'name');

     window.setTimeout(function() {
         console.log('name Value changed');
         selfVue.name = 'byebye world';
     },2000);
</script>

Then we open the page and it shows'hello world', which after 2s became'byebye world', a simple two-way binding implementation.

Comparing vue, we find a problem that when we assign values to attributes, the form is:'selfVue.data.name ='byebye world', while our ideal form is:'selfVue.name ='byebye world'. So how can we do this? Just do a proxy processing when new SelfVue, and let the property proxy accessing SelfVue access selfVue.datAttributes of a, the principle is to wrap the attributes on a layer using Object.defineProperty(). The code is as follows:

function SelfVue(data,el,exp) {
    var self = this;
    this.data = data;
    //The Object.keys() method returns an array of its enumerable properties for a given object
    Object.keys(data).forEach(function(key) {
        self.proxyKeys(key);     //Binding Agent Properties
    });
    observe(data);
    el.innerHTML = this.data[exp];   // Initialize the value of the template data
    new Watcher(this,exp,function(value) {
        el.innerHTML = value;
    });
    return this;
}
SelfVue.prototype = {
    proxyKeys:function(key) {
        var self = this;
        Object.defineProperty(this,key,{
            enumerable:false,
            configurable:true,
            get:function proxyGetter() {
                return self.data[key];
            },
            set:function proxySetter(newVal) {
                self.data[key] = newVal;
            } 
        });
    }
}

This allows us to change the template data in the ideal form.

(3) Implement Instruction Parser Compile

In the two-way binding demo above, we found that the whole process did not resolve the dom node, but fixed a node to replace the data, so we want to implement a parser Compile to parse and bind, analyze the role of the parser, and implement the following steps:

  • Parse template instructions, replace template data, initialize view

  • Bind the node corresponding to the template instruction to the corresponding update function to initialize the corresponding subscriber

In order to parse the template, first get the dom element, then process the nodes with instructions on the dom element. This process is cumbersome for the dom element, so we can create a fragment fragment fragment first, and save the parsed dom element in the fragment fragment fragment for processing:

nodeToFragment:function(el) {
    var fragment = document.createDocumentFragment();   //The createdocumentfragment() method creates a virtual node object that contains all the properties and methods.
    var child = el.firstChild;
    while(child) {
        // Move Dom elements into fragment s
        fragment.appendChild(child);
        child = el.firstChild;
    }
    return fragment;
}

Next, we need to iterate through all the nodes and do special processing on the nodes with instructions. First, let's deal with the simplest case, just the instructions with the form'{{variable}}', code as follows:

//Traverse through each node to make special handling of nodes with related designations
    compileElement:function(el) {
        var childNodes = el.childNodes;   //The childNodes property returns the collection of children of a node as a NodeList object.
        var self = this;
        //The slice() method returns the selected element from an existing array.
        [].slice.call(childNodes).forEach(function(node) {
            var reg = /\{\{(.*)\}\}/;
            var text = node.textContent;  //textContent property sets or returns the text content of the specified node
            if(self.isTextNode(node) && reg.test(text)) {      //Determine whether the instructions for {{}} are met
                //The exec() method retrieves matches for regular expressions in a string.
                //Returns an array containing matching results.If no match is found, the return value is null.
                self.compileText(node,reg.exec(text)[1]);
            }
            if(node.childNodes && node.childNodes.length) {
                self.compileElement(node);    //Continue recursive traversal of child nodes
            }
        });
    },
    compileText:function(node,exp) {
        var self = this;
        var initText = this.vm[exp];
        this.updateText(node,initText);    // Initialize the initialized data into the view
        new Watcher(this.vm,exp,function(value) {
            self.updateText(node,value);
        });

    },
    updateText:function(node,value) {
        node.textContent = typeof value == 'undefined' ? '': value;
    },

Once the outermost node is obtained, the compileElement function is called to judge all the child nodes. If the node is a text node and matches the {{}} form of instruction, the compilation process begins with the initialization of the view data, corresponding to step 1 above, followed by a subscriber that needs to generate and bind the update function, as described aboveStep 2.This completes the three processes of instruction parsing, initialization and compilation, and a parser Compile can work properly.

To associate the parser Compile with the listener Observer and subscriber Watcher, you need to modify the SelfVue-like functions:

function SelfVue(options) {
    var self = this;
    this.vm = this;
    this.data = options.data;
    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);     //Binding Agent Properties
    });
    observe(options.data);
    new Compile(options.el,this.vm);
    return this;
}

After the change, instead of having to bind two-way by passing in a fixed element value, you can freely name variables to bind two-way:

<body>
    <div id="app">
        <h1>{{title}}</h1>
        <h2>{{name}}</h2>
        <h3>{{content}}</h3>
    </div>
</body>
<script src="../js/observer2.js"></script>
<script src="../js/Watcher1.js"></script>
<script src="../js/compile1.js"></script>
<script src="../js/index3.js"></script>
<script>
    var selfVue = new SelfVue({
        el:'#app',
        data:{
            title:'aaa',
            name:'bbb',
            content:'ccc'
        }
    });
    window.setTimeout(function() {
        selfVue.title = 'ddd';
        selfVue.name = 'eee';
        selfVue.content = 'fff'
    },2000);
</script>

At this point, a data two-way binding function has been basically completed, and the next step is to improve the parsing and compilation of more instructions, where to process more instructions?
As long as the compileElement function mentioned above is combined with a judgment on other instruction nodes, then all its attributes are traversed to see if there are attributes of the matching instruction, and if so, it is parsed and compiled.Here we add a parsing compilation of v-model directives and event directives for which we use the function compile:

compile:function(node) {
        var nodeAttrs = node.attributes;   //The attributes attribute returns the set of attributes for the specified node, NamedNodeMap.
        var self = this;
        //The Array.prototype attribute represents the prototype of the Array constructor and allows new attributes and methods to be added to all Array objects.
        //Array.prototype itself is an Array
        Array.prototype.forEach.call(nodeAttrs,function(attr) {
            var attrName = attr.name;      //Add the method name and prefix of the event: v-on:click="onClick", then attrName ='v-on:click'id= "app" attrname='id'
            if(self.isDirective(attrName)) {     
                var exp = attr.value;      //Add the method name and prefix of the event: v-on:click='onClick', exp='onClick'

                //The substring() method extracts characters in a string between two specified subscripts.Return value is a new string
                //dir = 'on:click'
                var dir = attrName.substring(2);  
                if(self.isEventDirective(dir)) {   //Event directives
                    self.compileEvent(node,self.vm,exp,dir);
                }else {          //v-model directive
                     self.compileModel(node,self.vm,exp,dir);
                }
                node.removeAttribute(attrName);
            }
        });
    }

The compile function above mounts the Compile prototype. It first traverses through all the node attributes, then determines if the attribute is a directive attribute, and if so, distinguishes which directive it is, and then processes it accordingly.

Finally, we revamped SelfVue to look more like vue:

function SelfVue(options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);    
    });
    observe(options.data);
    new Compile(options.el,this);
    options.mounted.call(this);
}

Test it:

<body>
    <div id="app">
            <h2>{{title}}</h2>
            <input v-model="name">
            <h1>{{name}}</h1>
            <button v-on:click="clickMe">click me!</button>
    </div>
</body>
<script src="../js/observer3.js"></script>
<script src="../js/Watcher1.js"></script>
<script src="../js/compile2.js"></script>
<script src="../js/index4.js"></script>
<script>
    new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: 'canfoo'
        },
        methods: {
            clickMe: function () {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            window.setTimeout(() => {
                this.title = 'Hello';
            }, 1000);
        }
    });
</script>

The results are as follows:

151 original articles were published. 24 were praised. 30,000 visits+
Private letter follow

Posted by timc37 on Wed, 05 Feb 2020 19:00:06 -0800