Understand the principle and implementation of VUE two-way data binding

Original link:   https://www.jianshu.com/p/e7ebb1500613 

1. Principle:

1. Vue two-way data binding is achieved through data hijacking combined with publish and subscribe mode, that is to say, data and view are synchronized, data changes, and the view changes, the view changes, and the data also changes accordingly;

2. Core: Regarding VUE two-way data binding, its core is the Object.defineProperty() method;

3. Introduce the Object.defineProperty() method
(1) Object.defineProperty(obj, prop, descriptor). There are three parameters in this grammar, namely obj (the object whose properties are to be defined) prop (the one to be defined or modified) Property) descriptor (specific change method)
(2) Simply put, this method is used to define a value. When calling, we use the get method in it, and when we assign a value to this property, we use the set method in it;

Preliminary understanding of set and get methods

Second, first simply implement a js two-way data binding to familiarize yourself with this method:

 

<!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>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" id="a">
        <span id="b"></span>
    </div>
</body>
<script>
    var obj = {};  //定义一个空对象

    var val = 'zhao';  //赋予初始值

    Object.defineProperty(obj, 'val', {//定义要修改对象的属性

        get: function () {

            return val;
        },

        set: function (newVal) { 

             val = newVal;//定义val等于修改后的内容

            document.getElementById('a').value = val;//让文本框的内容等于val
            
            document.getElementById('b').innerHTML = val;//让span的内容等于val

        }

    });
    document.addEventListener('keyup', function (e) {//当在文本框输入内容时让对象里你定义的val等于文本框的值

        obj.val = e.target.value;

    })

</script>

</html>

In this way, we can realize the two-way data binding of js, and also have a preliminary understanding of this method ;
the effect of this example is: as the input text of the text box changes, the same text content will be displayed in the span synchronously; this is achieved The two-way binding of model => view and view => model.
The set method is triggered by adding the event listener keyup, and while the set modifies the accessor properties, it also modifies the dom style and changes the text in the span tag.

Three, to achieve a true two-way binding principle

1. Achieve the effect
Let's take a look at how vue two-way data binding is carried out, so that we can determine the direction of thinking

image

 

image

2. Task split

Splitting the task can make our thinking more clear:
(1) Bind the content of the data in vue to the input text box and text node
(2) When the content of the text box changes, the data in the vue instance is also at the same time Change
(3) When the content in data changes, the content of the input box and text node also changes

3. Start task 1-binding content
Let's first understand  the concept of DocuemntFragment (fragmented document) , you can think of it as a dom node container, when you create 10 nodes, when each node is inserted into the document It will trigger a browser reflow, which means that the browser has to reflow 10 times, which consumes resources.
Using fragmented documents means that I put all 10 nodes into a container first, and finally I insert the container directly into the document! The browser only reflowed once.
Note: Another very important feature is that if you use the appendChid method to add a node in the original dom tree to the DocumentFragment, the original node will be deleted.

For example:
you can see that there are two child nodes in my app, one element node and one text node.
However, when I hijack the data through DocumentFragment

image

image

image

Note: My fragmented document hijacks all the child nodes, and there is no content in the div whose id is app.
At the same time, we should mainly judge the conditions of while. Judge whether there are child nodes, because every time I appendChild I hijack the first child node in node, there will be one less node, until there is no child, the child becomes undefined, and the cycle is terminated. .

To achieve content binding,
we have to consider two issues, one is how to bind to the input, and the other is how to bind to the text node.
This way of thinking is here, we have obtained all the child nodes of the div, in the DocumentFragment, and then process each node to see if there is any content related to the vm instance, if so, modify this node content. Then add it back to DocumentFragment.

First, we write a function to process each node. If there is an input bound to the v-model attribute or a text node with { {xxx }} appears, replace the content with the content in the data in the vm instance

image

Then, when adding nodes to the fragmented document, each node is processed.

image

Create Vue's instantiation function

image

The renderings are as follows:

image

We successfully bound the content to the input box and text node!

4. Realize task 2-[view => model.
For this task, we consider the input box, the question of the input box, and how the input box changes data. We get the latest value through event listeners keyup, input, etc., and then assign the latest value obtained to the text of the instance vm through Object.defineProperty, and we pass the text under the data in the vm instance through Object.defineProperty Set as an accessor attribute, so assigning a value to vm.text triggers the set. One of the functions of the set function is to update the text in the data, and the other is to wait until the task is repeated.

First, implement a reactive monitoring function. Once a new value is assigned, it changes

image

Then, implement an observer to observe every attribute value of an instance.

image

Rewrite the compilation function. Note that the access method has also changed due to the change to the accessor attribute. At the same time, an event listener is added to update the text value of the instance at any time

image

In the example function, observe all the attribute values ​​in data, pay attention to the addition of observe

image

Finally, we can change the data in the input by changing the content in the input, but the single page is not refreshed.

image

image

4. Realize task 3-[model => view]
By modifying the properties of the vm instance, the content of the input box and the content of the text node should be changed.
There is a problem involved here that needs our attention. When we modify the input box, we change the attributes of the vm instance, which is one-to-one.
However, we may use the attributes in data in multiple places on the page, which is one-to-many. In other words, changing the value of one model can change the values ​​in multiple views.
This requires us to introduce a new knowledge point:

Subscription/Publisher Mode The
subscription publishing mode (also known as the observer mode) defines a one-to-many relationship, allowing multiple observers to monitor a certain subject object at the same time, and all observations will be notified when the state of the subject object changes者object.

The publisher sends a notification => the subject object receives the notification and pushes it to the subscriber => the subscriber performs the corresponding operation
1
For example:

image

The second function of the set function mentioned earlier is to remind subscribers to perform noticy operations, telling them: "My text has changed!" The text node becomes a subscriber. After receiving the message, immediately perform the update operation.

To recap, whenever a Vue is new, it mainly does two things: the first is to monitor data: observe(data), and the second is to compile HTML: nodeToFragement(id).
In the process of monitoring data, we will generate a theme object dep 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 container of the corresponding attribute.

We have implemented: 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's update method => update the view.
The key logic here is: how to add the watcher to the dep of the associated attribute.

Note: I changed the direct assignment operation to add a Watcher subscriber

image

So, what should Watcher do?

image

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 vm, which triggers the get method of the accessor attribute. In the get method, the watcher is added to the dep of the corresponding accessor attribute ;

Again, get the value of the attribute, and 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.

image

image

In the end we realized this two-way data binding function. Although it is very cumbersome, I believe that if you call it a few times, it will definitely help you, come on!!

Finally, the editor attaches the source code to everyone:

 

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>

    <body>

        <div id="app">

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

        </div>

    </body>

    <script type="text/javascript">
        //          编译函数
        function compile(node, vm) {

            var reg = /\{\{(.*)\}\}/; // 来匹配{
   
   {xxx}}中的xxx

            //如果是元素节点
            if(node.nodeType === 1) {

                var attr = node.attributes;

                //解析元素节点的所有属性

                for(let 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.data[name]; //将data中的值赋予给该node

                        node.removeAttribute('v-model')

                    }

                }

            }

            //如果是文本节点

            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 nodeToFragment(node, vm) {

            var fragment = document.createDocumentFragment();

            var child;

            while(child = node.firstChild) {

                compile(child, vm);

                fragment.appendChild(child);

            }

            return fragment

        }

        //          Vue构造函数     
        //      观察data中的所有属性值,注意增添了observe

        function Vue(options) {

            this.data = options.data;

            observe(this.data, this)

            var id = options.el;

            var dom = nodeToFragment(document.getElementById(id), this)

            //处理完所有节点后,重新把内容添加回去
            document.getElementById(id).appendChild(dom)

        }

        //      实现一个响应式监听属性的函数。一旦有赋新值就发生变化 

        function defineReactive(obj, key, val) {

            var dep = new Dep();

            Object.defineProperty(obj, key, {

                get: function() {

                    if(Dep.target) {

                        dep.addSub(Dep.target)

                    }

                    return val

                },
                set: function(newVal) {

                    if(newVal === val) {

                        return

                    }

                    val = newVal;

                    console.log('新值' + val);

                    //一旦更新立马通知

                    dep.notify();

                }

            })

        }

        //      实现一个观察者,对于一个实例 每一个属性值都进行观察。

        function observe(obj, vm) {

            for(let key of Object.keys(obj)) {

                defineReactive(vm, key, obj[key]);

            }

        }

        //      Watcher监听者

        function Watcher(vm, node, name) {

            Dep.target = this;

            this.vm = vm;
            this.node = node;
            this.name = name;

            this.update();

            Dep.target = null;

        }

        Watcher.prototype = {

            update() {
                this.get();
                this.node.nodeValue = this.value //更改节点内容的关键
            },
            get() {
                this.value = this.vm[this.name] //触发相应的get
            }

        }

        //      dep构造函数

        function Dep() {
            this.subs = []
        }
        Dep.prototype = {
            addSub(sub) {
                this.subs.push(sub)
            },
            notify() {
                this.subs.forEach(function(sub) {
                    sub.update();
                })
            }
        }

        var vm = new Vue({

            el: 'app',

            data: {
                text: '赵刚'
            }

        })
    </script>

</html>

Guess you like

Origin blog.csdn.net/terrychinaz/article/details/114123205