The principle of Vue two-way data binding (interview must ask) How to say Vue two-way binding interview

Vue.js adopts the method of data hijacking combined with publisher-subscriber mode, hijacks the setter and getter of each property through Object.defineProperty(), publishes a message to the subscriber when the data changes, and triggers the corresponding monitoring callback to render the view .

Specific steps

  • 1. Recursive traversal of the data object of the observer is required, including the attributes of the sub-attribute object, with setters and getters. In this case, assigning a value to this object will trigger the setter, and then the data change can be monitored
  • 2. compile parses the template instructions, replaces the variables in the template with data, then initializes the rendering page view, binds the node corresponding to each instruction with an update function, and adds a subscriber who listens to the data. Once the data changes, a notification is received , to update the view
  • 3. The Watcher subscriber is the communication bridge between Observer and Compile. The main things to do are:
    (1) Add itself to the attribute subscriber (dep) when instantiating itself
    (2) It must have an update() method
    (3) When the attribute change is notified by dep.notice(), it can call its own update() method, and trigger the callback bound in Compile, and then retire.
  • 4. MVVM is used as the entrance of data binding, integrates Observer, Compile and Watcher, monitors its own model data changes through Observer, parses and compiles template instructions through Compile, and finally uses Watcher to build a communication bridge between Observer and Compile , to achieve data change -> view update; view interactive change (input) -> two-way binding effect of data model change.

What is data two-way binding ?

Vue is an mvvm framework, that is, two-way data binding, that is, when the data changes, the view also changes, and when the view changes, the data also changes synchronously. This is also the essence of Vue. It is worth noting that  the two-way data binding we are talking about must be for UI controls, and non-UI controls will not involve two-way data binding. One-way data binding is a prerequisite for using state management tools such as redux. If we use vuex, the data flow is also single-item, which will conflict with two-way data binding.

Why implement two-way binding of data ?

In vue, if you use vuex, the data is actually one-way. The reason why it is called two-way data binding is that it is used for UI controls. For us to process forms, vue's two-way data binding is very comfortable to use up.

That is, the two are not mutually exclusive, and a single item is used in the global data flow, which is convenient for tracking; a bidirectional is used in the local data flow, which is simple and easy to operate.

1. What is Object.defineProperty?

1.1 Syntax:

Object.defineProperty(obj, prop, descriptor)

Parameter Description:

  1. obj: required. target
  2. props: Required. The name of the property to define or modify
  3. descriptor: Required. properties possessed by the target attribute

return value:

The object passed into the function. That is, the first parameter obj;

For attributes, we can set some characteristics for this attribute, such as whether it is read-only and not writable; whether it can be traversed by for...in or Object.keys().

Add feature description to the properties of the object, currently provide two forms: data description and accessor description.

When modifying or defining a property of an object, add some properties to this property:

1. Accessor property

The Object.defineProperty() function can define the property-related descriptor of an object. The set and get functions play a vital role in completing the two-way data binding. Let's see the basic usage of this function below.

var obj = {
      foo: 'foo'
    }

    Object.defineProperty(obj, 'foo', {
      get: function () {
        console.log('将要读取obj.foo属性');
      }, 
      set: function (newVal) {
        console.log('当前值为', newVal);
      }
    });

    obj.foo; // 将要读取obj.foo属性
    obj.foo = 'name'; // 当前值为 name

As you can see, get is called when we access the property, and set is called when we set the property value.

2. Simple two-way data binding implementation method

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
  <input type="text" id="textInput">
  输入:<span id="textSpan"></span>
  <script>
    var obj = {},
        textInput = document.querySelector('#textInput'),
        textSpan = document.querySelector('#textSpan');

    Object.defineProperty(obj, 'foo', {
      set: function (newValue) {
        textInput.value = newValue;
        textSpan.innerHTML = newValue;
      }
    });

    textInput.addEventListener('keyup', function (e) {
        obj.foo = e.target.value;
    });

  </script>
</body>
</html>

final rendering

It can be seen that it is not difficult to implement a simple two-way data binding: use Object.defineProperty() to define the set function of the property, when the property is assigned, modify the value of the Input and the innerHTML in the span; then monitor the input Such a simple two-way data binding can be realized by modifying the property value of the object in the keyup event.

The two-way binding directive is v-model:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue入门之htmlraw</title>
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <!-- v-model可以直接指向data中的属性,双向绑定就建立了 -->
    <input type="text" name="txt" v-model="msg">
    <p>您输入的信息是:{
   
   { msg }}</p>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        msg: '双向数据绑定的例子'
      }
    });
  </script>
</body>
</html>

The end result is: when you change the content of the input text box, the content in the p tag will change accordingly.

3. The idea of ​​realizing the task

Above we just implemented the simplest two-way data binding, but what we really hope to achieve is the following way:

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

    <script>
        var vm = new Vue({
            el: '#app', 
            data: {
                text: 'hello world'
            }
        });
    </script> 

That is, the two-way binding of data is realized in the same way as vue. Then, we can divide the whole implementation process into the following steps:

  • Input boxes and text nodes are bound to data in data
  • When the content of the input box changes, the data in data changes synchronously. That is,  the change of view => model .
  • When the data in data changes, the content of the text node changes synchronously. That is,  the change of model => view .

四、DocumentFragment

If we want to achieve task 1, we also need to use the DocumentFragment document fragment, which can be regarded as a container, as follows:

<div id="app">
        
    </div>
    <script>
        var flag = document.createDocumentFragment(),
            span = document.createElement('span'),
            textNode = document.createTextNode('hello world');
        span.appendChild(textNode);
        flag.appendChild(span);
        document.querySelector('#app').appendChild(flag)
    </script>

In this way, we can get the following DOM tree:

The advantage of using document fragments is that the DOM is operated on the document fragment without affecting the real DOM. After the operation is completed, we can add it to the real DOM, which is much more efficient than directly modifying the formal DOM. .

When vue compiles, it hijacks all the child nodes of the mount target into DocumentFragment, and after some processing, returns the DocumentFragment as a whole and inserts it into the mount target .

as follows  :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" id="a">
        <span id="b"></span>
    </div>

    <script>
        var dom = nodeToFragment(document.getElementById('app'));
        console.log(dom);

        function nodeToFragment(node) {
            var flag = document.createDocumentFragment();
            var child;
            while (child = node.firstChild) {
                flag.appendChild(child);
            }
            return flag;
        }

        document.getElementById('app').appendChild(dom);
    </script>

</body>
</html>

That is, first obtain the div, then hijack it through documentFragment, and then add the document fragment to the div.

5. Initialize data binding

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        {
   
   { text }}
    </div>
        
    <script>
        function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (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; // 获取v-model绑定的属性名
                        node.value = vm.data[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    node.nodeValue = vm.data[name]; // 将data的值赋值给该node
                }
            }
        }

        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;

            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }
            
            return flag;
        }

        function Vue(options) {
            this.data = options.data;
            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中。
            document.getElementById(id).appendChild(dom);
        }

        var vm  = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });


    </script>

</body>
</html>

The above code implements task 1. We can see that hello world has been presented in the input box and text node.

6. Responsive data binding

Let's take a look at the implementation idea of ​​task 2: When we input data in the input box, the input event (or keyup, change event) is first triggered, and in the corresponding event handler, we get the value of the input box and assign it Give the text attribute of the vm instance. We will use defineProperty to set the text in data as the accessor property of vm, so assigning a value to vm.text will trigger the set method. There are two main things to do in the set method. The first is to update the value of the attribute , and the second is to stay in the task.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        {
   
   { text }}
    </div>
        
    <script>
        function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (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; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发属性的set方法
                            vm[name] = e.target.value;
                        })


                        node.value = vm[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    node.nodeValue = vm[name]; // 将data的值赋值给该node
                }
            }
        }

        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;

            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }
            
            return flag;
        }

        function Vue(options) {
            this.data = options.data;
            var data = this.data;

            observe(data, this);

            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中。
            document.getElementById(id).appendChild(dom);
        }

        var vm  = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });



        function defineReactive(obj, key, val) {
            // 响应式的数据绑定
            Object.defineProperty(obj, key, {
                get: function () {
                    return val;
                },
                set: function (newVal) {
                    if (newVal === val) {
                        return; 
                    } else {
                        val = newVal;
                        console.log(val); // 方便看效果
                    }
                }
            });
        }

        function observe (obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            });
        }


    </script>

</body>
</html>

Above, task two is completed, and the text attribute value will change synchronously with the content of the input box.

7. Subscribe/publish mode (subscribe & publish)

The text property has changed, the set method is triggered, but the content of the text node has not changed. How can the text nodes that are also bound to text also change synchronously? Here is another knowledge point: subscription publishing mode.

The subscription-publishing mode, also known as the observer mode, defines a one-to-many relationship , allowing multiple observers to monitor a subject object at the same time . When the state of the subject object changes, all observer objects will be notified.

The publisher issues a notification  => the topic object receives the notification and pushes it to the subscriber => the subscriber performs the corresponding operation.

// 一个发布者 publisher,功能就是负责发布消息 - publish
        var pub = {
            publish: function () {
                dep.notify();
            }
        }

        // 多个订阅者 subscribers, 在发布者发布消息之后执行函数
        var sub1 = { 
            update: function () {
                console.log(1);
            }
        }
        var sub2 = { 
            update: function () {
                console.log(2);
            }
        }
        var sub3 = { 
            update: function () {
                console.log(3);
            }
        }

        // 一个主题对象
        function Dep() {
            this.subs = [sub1, sub2, sub3];
        }
        Dep.prototype.notify = function () {
            this.subs.forEach(function (sub) {
                sub.update();
            });
        }


        // 发布者发布消息, 主题对象执行notify方法,进而触发订阅者执行Update方法
        var dep = new Dep();
        pub.publish();

It is not difficult to see that the idea here is still very simple: the publisher is responsible for publishing the message, the subscriber is responsible for receiving and receiving the message, and the most important thing is the subject object, which needs to record all the people who subscribe to this special message, and then be responsible for publishing The message is notified to those who have subscribed to the message.

Therefore, the second thing to do when the set method is triggered is to issue a notification as the publisher: "I am the attribute text, and I have changed". Text nodes, as subscribers, perform corresponding update actions after receiving messages.

Eight, the realization of two-way binding

Looking back, whenever a new Vue is created, two things are mainly done: the first is to monitor data: observe(data), and the second is to compile HTML: nodeToFragment(id)

In the process of monitoring data, a theme object dep will be generated for each attribute in data.

In the process of compiling HTML, a subscriber watcher will be generated for each node related to data binding, and the watcher will add itself to the dep of the corresponding attribute.

We have achieved: modify the content of the input box => modify the attribute value in the event callback function => trigger the set method of the attribute.

The next thing we want to achieve is: send a notification dep.notify() => trigger the subscriber update method => update the view.

The key logic here is: How to add watcher to the dep of the associated attribute.

function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (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; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发属性的set方法
                            vm[name] = e.target.value;
                        })


                        node.value = vm[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    // node.nodeValue = vm[name]; // 将data的值赋值给该node

                    new Watcher(vm, node, name);
                }
            }
        }

During the process of compiling HTML, a Watcher is generated for each node associated with data. So what's going on in the Watcher function?

function Watcher(vm, node, name) {
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.update();
            Dep.target = null;
        }

        Watcher.prototype = {
            update: function () {
                this.get();
                this.node.nodeValue = this.value;
            },

            // 获取data中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的get
            }
        }

First, assign yourself to a global variable Dep.target;

Secondly, the update method is executed, and then the get method is executed. The get method reads the accessor attribute of the vm, thereby triggering the get method of the accessor attribute, and the get method adds the watcher to the dep of the corresponding accessor attribute;

Again, get the sequential value, then update the view.

Finally set Dep.target to empty. Because it is a global variable and the only bridge between watcher and dep, it must be guaranteed that Dep.target has only one value at any time.

Ultimately as follows

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text"> <br>
        {
   
   { text }} <br>
        {
   
   { text }}
    </div>
        
    <script>
        function 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 () {
                    // 添加订阅者watcher到主题对象Dep
                    if (Dep.target) {
                        dep.addSub(Dep.target);
                    }
                    return val;
                },
                set: function (newVal) {
                    if (newVal === val) {
                        return; 
                    } else {
                        val = newVal;
                        // 作为发布者发出通知
                        dep.notify()                        
                    }
                }
            });
        }
        
        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;

            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }
            
            return flag;
        }

        function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (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; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发属性的set方法
                            vm[name] = e.target.value;
                        })
                        node.value = vm[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    // node.nodeValue = vm[name]; // 将data的值赋值给该node

                    new Watcher(vm, node, name);
                }
            }
        }

        function Watcher(vm, node, name) {
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.update();
            Dep.target = null;
        }

        Watcher.prototype = {
            update: function () {
                this.get();
                this.node.nodeValue = this.value;
            },

            // 获取data中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的get
            }
        }

        function Dep () {
            this.subs = [];
        }

        Dep.prototype = {
            addSub: function (sub) {
                this.subs.push(sub);
            },

            notify: function () {
                this.subs.forEach(function (sub) {
                    sub.update();
                });
            }
        }

        function Vue(options) {
            this.data = options.data;
            var data = this.data;

            observe(data, this);

            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中。
            document.getElementById(id).appendChild(dom);
        }

        var vm  = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });

    </script>
</body>
</html>

 

Dachang interview questions share interview question bank

Front-end and back-end interview question banks (necessary for interviews) Recommended: ★★★★★

Address: front-end interview question bank   web front-end interview question bank VS java back-end interview question bank Daquan

Guess you like

Origin blog.csdn.net/weixin_42981560/article/details/131422679