美文网首页
Vue实现数据双向绑定的原理和方法

Vue实现数据双向绑定的原理和方法

作者: zzglovecoding | 来源:发表于2020-06-17 19:54 被阅读0次

一、实现双向绑定的一个极简方法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <input type="text" id="a">
    <span id="b"></span>
    <script type="text/javascript">
        var obj = {};
        Object.defineProperty(obj,'hello',{
            set:function(newVal) {
                document.getElementById('a').value = newVal;
                document.getElementById('b').innerHTML = newVal;
            }
        })
        document.addEventListener('keyup',function(e) {
            obj.hello = e.target.value;
        })
    </script>
</body>
</html>

上述代码实现的基本逻辑和功能

1.监听键盘抬起事件(这里我直接用的document监听,因为冒泡机制,document也会受到这个事件),触发就把当前事件的value给obj的hello属性
2.由于obj的hello属性有属性描述对象,所以在给这个hello属性赋值的时候,会触发set方法,从而把页面中id为a和b的两个节点的值都给改成了新的值,这里需要注意的一点是,id为a和b的两个节点,他们设置自身值的方法是不一样的,input可以设置value属性,但如果是其他的标签呢,比如span标签,你直接设置value等于某个值,是无效的,而要设置span的innerHTML(仅仅作一个友情提示~)

二、通过DocumentFragment实现view单向读取vue的数据

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model='text'>
        {{age}}
    </div>
    <script>
        var vm = new Vue({
            el:'app',
            data: {
                text:'helloWorld',
                name:'张三',
                age:23
            }
        })
        //上面模拟一个vue实例的创建过程,和vue写法一样
        function Vue(options) {
            this.data = options.data;
            var id = options.el;
            var outer = document.getElementById(id)
            var fragment = nodeToFragment(outer,this);
            document.getElementById(id).appendChild(fragment);
        }
        //上面是vue的构造函数
        function nodeToFragment(node,vm) {
            var fragment = document.createDocumentFragment();
            var child;
            while(child = node.firstChild) {
                compile(child,vm);
                fragment.append(child);
            }
            return fragment;
        }
        //node代表外层的节点,里面相当于是vue中el管理的那一片区域,vm就是vue实例
        //nodeToFragment的作用,就是把真实HTML中el管理的那一片区域,转成documentFragment,把里面的标签,按照绑定的情况从vue中把对应数据给documentFragment里头的节点
        function compile(node,vm) {
            var reg = /\{\{(.*)\}\}/
            if (node.nodeType === 1) {
                var attr = node.attributes;
                for(let i = 0;i < attr.length;i++) {
                    if (attr[i].nodeName === 'v-model') {
                        var value = vm.data[attr[i].nodeValue];
                        node.value = value;
                        node.removeAttribute('v-model')
                    }
                }
            }
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1;
                    name = name.trim();
                    node.nodeValue = vm.data[name];
                }
            }
        }
        //compile的作用,就是完成数据的转移,把vue实例里的数据,给到节点就可以了,node可能是文本节点,也可能是元素节点,所以根据不同的情况为其填入对应值
    </script>
</body>
</html>

上述代码实现的基本逻辑和功能

1.通过documentFragment将数据进行劫持,然后再添加到原来的元素中
2.compile对元素中v-model或者{{}}mustache语法进行识别,并把vue实例中的数据装进去
3.此时数据只是view把vue中的数据读取过来了,只是一个初始化,此时改变vue中的数据或者改变输入框,互相都不能实现同步

三、对v-model的节点添加事件,实现view到model的数据绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model='text'>
        {{age}}
    </div>
    <script>
        var vm = new Vue({
            el:'app',
            data: {
                text:'helloWorld',
                name:'张三',
                age:23
            }
        })
        function Vue(options) {
            this.data = options.data;
            var id = options.el;
            var data =options.data;

            observe(data,this)

            var outer = document.getElementById(id)
            var fragment = nodeToFragment(outer,this);
            document.getElementById(id).appendChild(fragment);
        }
        function observe(dataObj,vm) {
            Object.keys(dataObj).forEach((key) => {
                makeItReactiveOnVM(vm,key,dataObj[key]);
            })
        }
        //上述代码把data对象里面所有属性和值遍历并加到了vm上面
        function makeItReactiveOnVM(vm,key,value) {
            Object.defineProperty(vm,key,{
                get:function() {
                    return value;
                },
                set:function(newValue) {
                    if (newValue === value) return;
                    value = newValue;
                }
            })
        }
        //makeItReactiveOnVM就把data对象的每个属性,都弄到了vue实例中,并且实现了set和get方法
        function nodeToFragment(node,vm) {
            var fragment = document.createDocumentFragment();
            var child;
            while(child = node.firstChild) {
                compile(child,vm);
                fragment.append(child);
            }
            return fragment;
        }
        function compile(node,vm) {
            var reg = /\{\{(.*)\}\}/
            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;
                        })
                        //这里顺道给这个v-model的元素加上监听事件,如果值变了,就要改vue实例中的数据
                        node.value = vm[name];
                        //之前observe把数据直接给了vue实例,所以不用再vm.data[name]了
                        node.removeAttribute('v-model')
                    }
                }
            }
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1;
                    name = name.trim();
                    //node.nodeValue = vm.data[name];
                    node.nodeValue = vm[name]
                }
            }
        }
    </script>
</body>
</html>

上述代码实现的基本逻辑和功能

1.在compile也就是数据初次渲染的时候,就给v-model的元素添加了监听事件,如果发生了input就把数据给vue
2.在上面添加了observe和makeItReactiveOnVM方法,把数据直接弄到了vue实例下面,而不是vue的data下面了,并且为每个属性都设置了set和get方法,方便我们下一步实现vue中数据的改变,同步到输入框中数据改变

四、为vue的每个属性添加Dep对象,并在初次时,设置watcher实现发布订阅者模式的双向绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model='text'>
        {{age}}
    </div>
    <script>

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

            observe(data,this)
            var id = options.el;
            var outer = document.getElementById(id)
            var fragment = nodeToFragment(outer,this);
            document.getElementById(id).appendChild(fragment);
        }
        function observe(dataObj,vm) {
            Object.keys(dataObj).forEach((key) => {
                makeItReactiveOnVM(vm,key,dataObj[key]);
            })
        }
        function makeItReactiveOnVM(vm,key,value) {
            var dep = new Dep();
            Object.defineProperty(vm,key,{
                get:function() {
                    if (Dep.target) {
                        //能进入到这里,说明是在调用new watcher,因为Dep.target有值
                        dep.addSub(Dep.target);
                    }
                    return value;
                },
                set:function(newValue) {
                    if (newValue === value) return;
                    value = newValue;
                    dep.notify();
                }
            });
        }
        function nodeToFragment(node,vm) {
            var fragment = document.createDocumentFragment();
            var child;
            while(child = node.firstChild) {
                compile(child,vm);
                fragment.appendChild(child);
            }
            return fragment;
        }
        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
                        node.addEventListener('input',function(e) {
                            vm[name] = e.target.value;
                        })
                        new watcher(vm,node,name,'input');
                        node.removeAttribute('v-model')
                    }
                }
                
            }
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1;
                    name = name.trim();
                    //node.nodeValue = vm[name]
                    new watcher(vm,node,name,'text');
                }
            }
        }

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

        watcher.prototype = {
              update: function () {
                this.get();
                if (this.nodeType == 'text') {
                  this.node.nodeValue = this.value;
                }
                if (this.nodeType == 'input') {
                  this.node.value = this.value;
                }
              },
              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();
                });
            }
        }
        var vm = new Vue({
            el:'app',
            data: {
                text:'helloWorld',
                name:'张三',
                age:23
            }
        })
    </script>
</body>
</html>

上述代码的实现逻辑和功能

1.每个属性的描述对象内,都有一个dep对象,dep对象内有所有订阅了这个属性值的对象,存在subs数组中
2.在compile的时候,就依据使用该值的node,name,vm,nodeType创建一个watcher,从而给node赋值,使用的该watcher的update方法触发了对应属性的get方法,从而在其dep对象里面保存了该watcher对象
3.非常要注意的是代码的顺序,new vue操作是在最后,如果放在第一行,function这些会提前,但是watcher.prototype不会提前,所以调用update方法会出问题,这一点需要注意
4.model到view层,如果数据改变,会调用notify方法通知所有的watcher,watcher会重新获取vue的值到watcher对象中,并操控元素改变相关的值,并且dep不会重复添加watcher,因为watcher只有在被new的时候,Dep.target才会指向新创建的watcher对象,后面在更新的之后,只会调用其update方法更新数据

总结:
实现双向绑定主要是两个步骤
1.把vue实例中的数据监听,为vue实例中每一条data的key通过defineProperty劫持,使得这些数据挂载在vue实例上,并且每个属性的get和set都有对应的dep在管理,如果get,如果set就有对应的行为。
2.把页面上的v-model,{{}}都通过compile给解析出来,弄到fragment里头再放回去,compile的时候,会在这些对应的node上,都设置一个watcher,watcher里保存了vue实例,如果变化了,dep会通知watcher调用update方法,此时再从vue中拿数据更新即可。
总之,就是vue实例先实现数据的劫持,然后解析页面,页面中受监控的节点如果变动,就会通知到vue,vue又会通知所有用了这个属性的watcher。在数据端,有一个dep在管理,在页面那里,有一个watcher在管理。

注:本文是自我的理解和梳理,在看相关博客的时候,有一些费解和没看懂的地方,自己进行了整理,然后自己逐步照着实现了一遍,把完整的代码也贴上了,有问题欢迎交流~

相关文章

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • vue 双向数据绑定

    Vue实现数据双向绑定的原理:Object.defineProperty()vue实现数据双向绑定主要是:采用数据...

  • Vue实现数据双向绑定的原理

    Vue实现数据双向绑定的原理:Object.defineProperty() vue实现数据双向绑定主要是:采用数...

  • 【转】JavaScript的观察者模式(Vue双向绑定原理)

    关于Vue实现数据双向绑定的原理,请点击:Vue实现数据双向绑定的原理原文链接:JavaScript设计模式之观察...

  • 前端理论面试--VUE

    vue双向绑定的原理(详细链接) VUE实现双向数据绑定的原理就是利用了 Object.definePropert...

  • 前端面试题:VUE

    1. vue的双向数据绑定实现原理? 2. vue如何在组件之间进行传值? 3. vuex和vue的双向数据绑定...

  • vue

    1、vue的双向数据绑定实现原理 2、vue如何在组件之间进行传值 3、vuex和vue的双向数据绑定有什么冲突 ...

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • vue面试知识点

    vue 数据双向绑定原理 vue实现数据双向绑定原理主要是:采用数据劫持结合发布订阅设计模式的方式,通过对data...

网友评论

      本文标题:Vue实现数据双向绑定的原理和方法

      本文链接:https://www.haomeiwen.com/subject/mfcexktx.html