preface

Many people have asked during the interview process principle and the implementation of the Vue two-way binding, it is a cliche of interview questions, although there are also many online two-way binding article, but I think after for most front small white, is not very easy to understand, so, this article I’ll use the simplest code to teach you how to implement a Vue two-way binding.

The principle of bidirectional binding

Used Vue framework knows that page at the time of initialization, we can render the attributes in the data to the page, change the data on the page, a corresponding of the attributes in the data update, this is what we call the two-way binding, so, in simple terms, we want to achieve a two-way binding in order to achieve the following 3 points of operation:

  1. The first step is to parse the code when Vue is instantiatedv-modleInstructions and{{}}Instruction, and then bind the properties in data to the corresponding instruction, so we’re going to implement a parser Compile, that’s number one;
  2. And then when we change the properties of the page, we need to know which properties have changed, and then we need to useObject.definePropertyIn thegettersetterMethod hijacks properties, here we implement a monitor Observer, that’s two;
  3. Once we know which properties have changed, we need to execute the corresponding function to update the view. In this case, we need to implement a message subscription, subscribe to each property when the page is initialized, and subscribe to each property in theObject.definePropertyData hijacking receives notification of property changes and updates to the view, so we implement a subscriber Watcher, which is the third point.

1. Realize the Compile

First, let’s start with the basic parsing instructions. Without further ado, let’s start with the code:

v-model
{{}}

v-model
{{}}

<! DOCTYPE html> <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>MVVMdemo</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="text">
        <div>{{text}}</div>
    </div>
</body>
<script type="text/javascript">
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world',}})functionVue(options) { this.data = options.data; var id = options.el; var dom = nodeToFragment(document.getElementById(id), Document.getelementbyid (id).appendChild(dom); // Add the processed DocumentFragment back to the Dom}function nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            flag.appendChild(child)
        }
        returnFlag} // Parse the nodefunctioncompile(node, vm) { var reg = /\{\{(.*)\}\}/; // Determine whether there are child nodesif (node.childNodes && node.childNodes.length) {
            node.childNodes.forEach(function (node) {
                compile(node, vm)
            })
        } else{// Parse the V-modelif (node.nodeType === 1) {
                var attr = node.attributes;
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == "v-model") { var name = attr[i].nodeValue; node.value = vm.data[name]; // Assign data to node node.removeAttribute('v-model'); // Remove the v-model attribute}}; } // parse {{}}if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.The $1; // Get the matching string name = name.trim(); node.nodeValue = vm[name] } } } } </script> </html>Copy the code

The above code is a simple way to parse instructions. Let me explain briefly:

  1. document.createDocumentFragment()

    document.createDocumentFragment()An empty container is used to create a virtual node object. What we need to do here is to parse the corresponding instruction while traversing the node. After parsing an instruction, add it tocreateDocumentFragmentThe advantage of this is to reduce the number of times the page renders the DOM. Please refer to the documentation for more detailsCreateDocumentFragment (
  2. function compile (node, vm)

    compile()Method to determine each node, first determine whether the node contains child nodes, if so, continue to call compile() method for parsing. If you don’t have one, you can determine the type of nodeElement typeandText Text element type, and then parse the two types separately.

After completing the above steps, our code can display normally on the page, however, there is a problem, our page is bound to the data in the data, but when changing the data in the input box, the corresponding data in the data is not synchronized update. So, next we hijack the update of the data, hijacking the corresponding property change in the data via Object.defineProperty().

2. Implement the Observer

To implement bidirectional binding of data, we need to implement data hijacking via Object.defineProperty(), listening for property changes. So let’s start with a simple example to see how Object.defineProperty() works.

var obj ={};
var name="hello";
Object.defineProperty(obj,'name',{
    
    get:function(val) {// Get the attribute console.log('Get method called');
        
        return name 
    },
    set:function(val) {// Set the property console.log('Set method called');
        name=val  
    }
})
console.log(obj.name);
obj.name='hello world'
console.log(obj.name);
Copy the code

Running the code, we can see the console output:

Object.defineProperty( )
Object.defineProperty()
DefineProperty () use tutorial


<! DOCTYPE html> <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>MVVMdemo</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="text">
        <div>{{text}}</div>
    </div>
</body>
<script type="text/javascript">
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world',}})functionVue(options) { this.data = options.data; var id = options.el; observe(this.data,this); Var dom = nodeToFragment(document.getelementById (id), Document.getelementbyid (id).appendChild(dom); // Add the processed DocumentFragment back to the Dom}function nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            flag.appendChild(child)
        }
        returnFlag} // Parse the nodefunctioncompile(node, vm) { var reg = /\{\{(.*)\}\}/; // Determine whether there are child nodesif (node.childNodes && node.childNodes.length) {
            node.childNodes.forEach(function (node) {
                compile(node, vm)
            })
        } else{// Parse the V-modelif (node.nodeType === 1) {
                var attr = node.attributes;
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == "v-model") {
                        var name = attr[i].nodeValue;
                        node.addEventListener('input'.function(e){ vm[name]=e.target.value; }) node.value= vm[name]; // Assign data to node node.removeAttribute('v-model'); // Remove the v-model attribute}}; } // parse {{}}if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.The $1; // Get the matching string name = name.trim(); node.nodeValue = vm[name] } } } }function defineReactive(obj,key,val) {
        Object.defineProperty(obj,key,{
            get:function() {
                return val;
            },
            set:function(newval) {
                if(newval === val) return; val = newval; console.log(val); // Prints (listens for data changes)}})} // recursively traverses all data attributesfunction observe(obj,vm) {
        Object.keys(obj).forEach(function(key){
            defineReactive(vm,key,obj[key])
        })
    }
</script>

</html>
Copy the code

We recursively traverse all of data’s child attributes during page initialization, adding a monitor to each attribute, and firing the set method in defineProperty() when listening for data changes. We can see the set method listening for attribute changes in console output.

3. Realize the Watcher

A lot of people have seen other MVVM implementations on the web, but they don’t know much about Watcher subscribers. In fact, apart from the code, the Watcher implementation function is actually very simple, which is that when the Vue is instantiated, each attribute is injected with a subscriber Watcher. It is convenient to listen for property acquisition in Object.defineProperty() data hijacking (get method), and notify the update via Watcher when Object.defineProperty() is listening for data changes (set method), so in a nutshell, Watcher acts as a bridge. Having listened for data changes with Object.defineProperty() above, we will implement Watcher for the final step of bidirectional binding.

<! DOCTYPE html> <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>MVVMdemo</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="text">
        <div>{{text}}</div>
    </div>
</body>
<script type="text/javascript">

    functionVue(options) { this.data = options.data; var id = options.el; observe(this.data, this); Var dom = nodeToFragment(document.getelementById (id), Document.getelementbyid (id).appendChild(dom); // Add the processed DocumentFragment back to the Dom}function nodeToFragment(node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            flag.appendChild(child)
        }
        returnFlag} // Parse the nodefunctioncompile(node, vm) { var reg = /\{\{(.*)\}\}/; // Determine whether there are child nodesif (node.childNodes && node.childNodes.length) {
            node.childNodes.forEach(function (node) {
                compile(node, vm)
            })
        } else{// Parse the V-modelif (node.nodeType === 1) {
                var attr = node.attributes;
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == "v-model") {
                        var name = attr[i].nodeValue;
                        node.addEventListener('input'.function(e) { vm[name] = e.target.value; }); node.value = vm[name]; // Assign data to node node.removeAttribute('v-model'); // Remove the v-model attribute}}; new Watcher(vm, node, name,'input'); // Generate a new Watcher, labeled input} // parse {{}}if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.The $1; // Get the matching string name = name.trim(); new Watcher(vm, node, name,'text'); // Generate a new Watcher, marked as text}}}} // to recursively traverse all data attributesfunction observe(obj, vm) {
        Object.keys(obj).forEach(function (key) {
            defineReactive(vm, key, obj[key])
        })
    }
    function defineReactive(obj, key, val) {
        var dep = new Dep();
        Object.defineProperty(obj, key, {
            get: function() {// Add subscriber watcher to topic object Dep;if (Dep.target) dep.addSub(Dep.target);
                return val
            },
            set: function (newVal) {
                if (newVal === val) returnval = newVal; Dep.notify (); }}); } // Collect all initialized generated subscribers into an arrayfunction Dep() {
        this.subs = []
    }
    Dep.prototype = {
        addSub: function (sub) {
            this.subs.push(sub)
        },
        notify: function () {
            this.subs.forEach(function(sub) { sub.update(); })}} // Subscriber WatcherfunctionWatcher(vm, node, name, nodeType) { Dep.target = this; this.name = name; this.node = node; this.vm = vm; this.nodeType = nodeType; this.update(); Dep.target = null; } prototype = {// execute the corresponding update function:function () {
            this.get();
            if (this.nodeType == 'text') {
                this.node.nodeValue = this.value;
            }
            if (this.nodeType == 'input') { this.node.value = this.value; }}, // get the property value of data:function() { this.value = this.vm[this.name]; // Trigger get}} </script> <scripttype="text/javascript">
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world',
        }
    })
</script>
</html>
Copy the code

We added a subscriber Watcher and a message collector Dep to the code in step 2, and I’ll show you what they did. First of all:

functionWatcher(vm, node, name, nodeType) { Dep.target = this; this.name = name; this.node = node; this.vm = vm; this.nodeType = nodeType; this.update(); Dep.target = null; } prototype = {// execute the corresponding update function:function () {
            this.get();
            if (this.nodeType == 'text') {
                this.node.nodeValue = this.value;
            }
            if (this.nodeType == 'input') { this.node.value = this.value; }}, // get the property value of data:function() { this.value = this.vm[this.name]; // Trigger get}}Copy the code

The Watcher() method takes the vm instance, node node object, name name of the passed node type, and nodeType node type. First, it assigns itself to a global variable, dep.target;

Second, the update method is executed, and then the GET method is executed. The GET method reads the accessor attribute of vm, which triggers the GET method of accessor attribute. The GET method adds the watcher to the DEP of the corresponding accessor attribute.

Again, get the value of the property, and then update the view.

Finally, set dep. target to null. Because it is a global variable and watcher’s only bridge to the DEP, the deP. Target must have only one value at all times.

On instantiation, we add a Watcher() subscriber for each attribute, store the subscriber for each attribute in the Dep array when observe () ‘s listener attribute is assigned, and call dep.notify() to notify Watcher() to update the data when set is triggered. Finally, the view is updated.

4. Conclusion

The above is the basic implementation principle and code of Vue bidirectional binding, of course, this is just the basic implementation code, simple and intuitive show to you, if you want to understand more deeply, recommend you to read this article Vue bidirectional binding principle and implementation.

Ok, the above is this time to share, I hope to help you understand Vue bidirectional binding, but also hope that you have any don’t understand or suggestions, you can leave a message interaction.