vue响应式原理数据双向绑定的原理

先来了解一下几个相关概念

1、渐进式框架

下面是摘自知乎的一个解答(个人认为讲述比较好的回答):

在我看来,渐进式代表的含义是:主张最少
每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式。
比如说,Angular,它两个版本都是强主张的,如果你用它,必须接受以下东西:
- 必须使用它的模块机制- 必须使用它的依赖注入- 必须使用它的特殊形式定义组件(这一点每个视图框架都有,难以避免)

所以Angular是带有比较强的排它性的,如果你的应用不是从头开始,而是要不断考虑是否跟其他东西集成,这些主张会带来一些困扰。

比如React,它也有一定程度的主张,它的主张主要是函数式编程的理念,比如说,你需要知道什么是副作用,什么是纯函数,如何隔离副作用。它的侵入性看似没有Angular那么强,主要因为它是软性侵入。

Vue可能有些方面是不如React,不如Angular,但它是渐进的,没有强主张,你可以在原有大系统的上面,把一两个组件改用它实现,当jQuery用;也可以整个用它全家桶开发,当Angular用;还可以用它的视图,搭配你自己设计的整个下层用。你可以在底层数据逻辑的地方用OO和设计模式的那套理念,也可以函数式,都可以,它只是个轻量视图而已,只做了自己该做的事,没有做不该做的事,仅此而已。

渐进式的含义,我的理解是:没有多做职责之外的事

2、MVC模式

MVC的全称是Model-View-Controller,模型-视图-控制器,整个结构分成三层

● 最上面一层,视图层(View):用户界面(UI)

● 最底层,是核心的“数据层”:保存数据

● 中间层,控制层(Controller):处理业务逻辑,负责根据用户从“视图层”输入的指令,选取“数据层”的数据,然后对其进行相应的操作,产生最终的结果

各部分的通信方式如下:

A、View传送指令到Controller

B、Controller完成业务逻辑后,要求Model改变状态

C、Model将新的数据发送到View,用户得到反馈

所有通信都是单向的

栗子:

以计算机的计算器为例,解释一下MVC模式(不一定使用这种模式编写):

外部的按钮和最上面的显示条就是View(视图层);需要运算的数字就是Model(数据层);执行+ - * /等内部运算步骤就是Controller(控制层)。每一层都执行不同的功能。

详细讲解的链接

3、MVP

MVP模式将Controller更名为Presenter,同时改变了通信方向

A、各部分的通信都是双向的

B、View与Model不发生联系,都通过Presenter传递

C、View不部署任何业务逻辑,成为“被动视图”,而所有业务逻辑都部署在Presenter

4、MVVM模式

MVVM模式将Presenter更名为ViewModel(对应MVC中的C-controller),基本上与MVP模式一致。唯一区别MVVM采用双向数据绑定,View的变动自动反应在ViewModel上。

● M(model):模型---javascript object,代表真实情况的内容(一个面向对象的方法)、或表示内容(以数据为中心的方法)的数据访问层

● V(view):视图---用户界面(UI)

● Viewmodel:在vue中指vue实例对象,是一个公开公共属性和命令的抽象的view;是一个转值器,负责转换Model中的数据对象,来让对象变得更容易管理和使用。

View的变化会自动更新到ViewModel,ViewModel的变化也会自动同步到View上显示。这种自动同步是因为ViewModel中的属性实现了Observer,当属性变更时都能触发对应的操作。

 

5、轻量级框架的定义详细介绍链接

6、数据双向绑定

所谓的双向绑定,就是view的变化能反映到ViewModel上,ViewModel的变化能同步到view上

详细参考链接

vue的定义

● vue是一套用于构建用户界面的渐进式框架

vue是一款基于MVVM方式的轻量级的框架

vue是一款基于数据驱动、组件化思想的框架

vue被设计为可以自底向上、逐层应用的框架

● vue的核心库只关注视图层,易于上手,还便于与第三方库或既有项目整合

● 当与现代化的工具链以及各种支持类库结合使用时,vue也完全能够为复杂的单页应用提供驱动

 

数据驱动:Vue.js 一个核心思想是数据驱动。所谓数据驱动是指视图是由数据驱动生成的,对视图的修改不会直接操作 DOM,而是通过修改数据。相比传统的前端开发,如使用 jQuery 等前端库直接修改 DOM大大简化了代码量,特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护

在MVVM中,M(model)---代表JavaScript  Objects,V(view)---DOM也就是UI,VM(ViewModel)----代表Vue实例对象(在该对象中有Directives和DOM Listeners)

在vue.js里面只需要改变数据,Vue.js通过Directives指令对DOM做封装,当数据发生变化,会通知指令修改对应的DOM,数据驱动DOM的变化,DOM是数据的一种自然的映射。vue.js还会对View操作做一些监听(DOM Listener),当我们修改视图的时候,vue.js监听到这些变化,从而改变数据。这样就形成了数据的双向绑定。

 

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

如右图所示,new Vue一个实例对象a,其中有一个属性a.b,那么在实例化的过程中,通过Object.defineProperty()会对a.b添加getter和setter,同时Vue.js会对模板做编译,解析生成一个指令对象(这里是v-text指令),每个指令对象都会关联一个Watcher,对a.b求值的时候,就会触发它的getter,当修改a.b的值的时候,就会触发它的setter,同时会通知被关联的Watcher,然后Watcher就会再次对a.b求值,计算对比新旧值,当值改变了,Watcher就会通知到指令,调用指令的update()方法,由于指令是对DOM的封装,所以就会调用DOM的原生方法去更新视图,这样就完成了数据改变到视图更新的一个自动过程

 

大致上是使用数据劫持和订阅发布实现双向绑定。通过实例化一个Vue对象的时候,对其数据属性遍历,通过Object.defineProperty()给数据对象添加setter  getter,并对模板做编译生成指令对象,每个指令对象绑定一个watcher对象,然后对数据赋值的时候就会触发setter,这时候相应的watcher对其再次求值,如果值确实发生变化了,就会通知相应的指令,调用指令的update方法,由于指令是对DOM的封装,这时候会调用DOM的原生方法对DOM做更新,这就实现了数据驱动DOM的变化。同时vue还会对DOM做事件监听,如果DOM发生变化,vue监听到,就会修改相应的data

 

 

实现数据双向绑定的方法

A、发布者-订阅者模式(backbone.js)

思路:使用自定义的data属性,在HTML代码中指明绑定。所有绑定起来的javascript对象以及DOM元素都将订阅一个发布者对象。任何时候如果javascript对象或者一个HTML输入字段被侦测到发生变化,将代理事件变成发布者-订阅者模式,这会反过来变化广播,并传播到所有绑定的javascript对象以及DOM元素上。

B、脏值检查(angular.js):dirty check   详细讲解链接

angular.js是通过脏值检测的方式,对比数据是否有变更,从而决定是否更新视图。最简单的方式就是通过setInterval()定时轮询检测数据变动。angular.js只有在指定的事件触发时,进入脏值检测,大致如下:

● DOM事件,譬如用户输入文本,点击按钮等(ng-click)

● XHR响应事件($http)

● 浏览器location变更事件($location)

● Timer事件($timeout,$interval)

● 执行$digest()或$apply()

C、数据劫持结合发布者-订阅者模式(vue.js)【vue data是如何实现的??】

vue.js采用数据劫持结合发布者-订阅者的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时,发布消息给订阅者,触发相应的监听回调。

具体的来讲,Vue.js通过Directives指令去对DOM做封装,当数据发生变化,会通知指令去修改对应的DOM,数据驱动DOM的变化。vue.js还会对操作做一些监听(DOM Listener),当我们修改视图的时候,vue.js监听到这些变化,从而改变数据。这样就形成了数据的双向绑定。

具体步骤如下:

● 首先,需要对observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter  getter。这样的话,给这个对象的某个属性赋值,就会触发setter,那么就能监听到数据变化。(其实是通过Object.defineProperty()实现监听数据变化的)

● 然后,需要compile解析模板指令,将模板中的变量替换成数据,接着初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者。一旦数据有变动,订阅者收到通知,就会更新视图

● 接着,Watcher订阅者是Observer和Compile之间通信的桥梁,主要负责:

         1)在自身实例化时,往属性订阅器(Dep)里面添加自己

         2)自身必须有一个update()方法

         3)待属性变动,dep.notice()通知时,就调用自身的update()方法,并触发Compile中绑定的回调

● 最后,viewmodel(vue实例对象)作为数据绑定的入口,整合Observer、Compile、Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 (ViewModel)-》视图更新(view);视图变化(view)-》数据(ViewModel)变更的双向绑定效果。

例1:利用Object.defineProperty()实现一个小栗子(G:\有用的\练习\a.js)

var obj = {};
Object.defineProperty(obj,'hello',{ //obj---变量名,hello---变量的属性名(任意取名)
  get: function() {
    console.log('get val:'+ value);    //value-(任意取名)--属性“hello”的值
    return value;
   },  set: function(newVal) {
    value = newVal;
    console.log('set val:'+ value);
  }
});

obj.hello='111';
obj.hello;

结果:

set val:111
get val:111

如果去掉下面这句话

obj.hello='111';

则控制台报错:

 

ReferenceError: value is not defined

可见Object.defineProperty()监控对数据的操作,可以自动触发数据同步

 

 

 

例2、利用Object.defineProperty()实现简单的双向绑定(G:\有用的\练习\a.html

<!DOCTYPE html>
<head>
  <title>测试Object.defineProperty()</title>
</head>
<body>
   <div id="app">
      <input type="text" id="a">
      <span id="b"></span>
   </div>
 
   <script type="text/javascript">
        var obj = {};
        Object.defineProperty(obj,{
            get: function() {
              console.log('get val:'+ val);
              return val;
            },set: function(newVal) {
              val = newVal;
              console.log('set val:'+ val);
              document.getElementById('a').value = val;
              document.getElementById('b').innerHTML = val;
            }
        });
        document.addEventListener('keyup',function(e) {//触发事件的时机,从而执行相应的操作
           obj.hello = e.target.value;
        });
    </script>
 </body>
</html>

效果如下:

上面操作直接使用了DOM操作改变了文本节点的值,而且是在知道id的情况下,使用document.getELementById()获取到响应的文本节点,然后修改文本节点的值。

封装成一个框架,肯定不能使用这种做法。所以需要一个可以解析DOM并且能修改DOM中相应变量的模块。

例3、实现简单CompileG:\有用的\练习\b.html

A、首先,获取文本中真实的DOM节点

B、然后,分析节点的类型

C、最后,根据节点的类型做相应的处理

在例2中,多次操作DOM节点,为了提高性能和效率,将进行下面操作:

A、将所有的节点转换成文档碎片(fragment)进行编译操作

B、解析操作完成后,在将文档碎片(fragment)添加到原来的真实DOM节点

<!DOCTYPE html>
<head></head>
<body>
    <div id="app">
        <input type="text" id="a" v-model="text">  <!-- v-model指令实现文本输入与应用状态之间的双向绑定 -->
        {{text}}     <!--data中的某个属性名-->
    </div>


   <script type="text/javascript">


        function Compile(node,vm) { //node---DOM中的某个节点,vm---用Vue()实例化的对象
            if(node) {//判断该节点是否存在,存在则将节点转换成文档碎片
                this.$frag = this.nodeToFragment(node,vm);
                return this.$frag;//返回文档碎片
            }
        }


        Compile.prototype = {
            nodeToFragment: function(node,vm) {//将node节点的所有的子节点都转换成fragment
                var self = this;
                var frag = document.createDocumentFragment();
                var child;
     
                while(child = node.firstChild) {
                     self.compileElement(child,vm);
                     frag.append(child); //  // 将所有子节点添加到fragment中,child是指向元素首个子节点的引用。将child引用指向的对象append到父对象的末尾,原来child引用的对象就跳到了frag对象的末尾,而child就指向了本来是排在第二个的元素对象。如此循环下去,链接就逐个往后跳了
                }
                return frag;
            },            compileElement: function(node,vm) {//分析节点的类型,并处理节点中相应的变量的值
                var reg = /\{\{(.*)\}\}/;//.匹配除了\b之外的任何单个字符,*表示0或多个,形如{{d}}
         
                //节点类型为元素
                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.data[name]= e.target.value;
                            });
                            node.value = vm.data[name]; // 将data的值赋给该node
                            node.removeAttribute('v-model');
                        }
                    }
                }
                //节点类型为text
                if(node.nodeType === 3) {
                    if(reg.test(node.nodeValue)) {// 获取v-model绑定的属性名
                        var name = RegExp.$1; // 获取匹配到的字符串
                        name = name.trim();
                        node.nodeValue = vm.data[name]; // 将data的值赋给该node
                       // new Watcher(vm,node,name);
                    }
                }
            },        }


        // 实例化对象的构造函数
        function Vue(options) {
            this.data = options.data;
            var data = this.data;   //vue实例对象的data对象属性
            var id = options.el;   //挂载元素的id
            var dom =new Compile(document.getElementById(id),this);
            // 编译完成后,将dom返回到app中
            document.getElementById(id).appendChild(dom);
        }


        var vm = new Vue({ //利用构造函数Vue(),实例化一个对象vm
            el: 'app',     //el---挂载元素,app---表示DOM中的某个节点的id
            data: {        //data---进行交互的数据
                text: 'hello world'  //属性text名与v-model绑定的属性名一样
            }
        });
   </script>
 </body>
</html>

到这我们获取了文本中真实的DOM节点,然后分析节点的类型,并能处理节点中相应的变量,如上面的{{text}},最后渲染到页面中。最后,需要和双向绑定联系起来,实现{{text}}响应式的数据绑定

 

例4、实现简单observeG:\有用的\练习\c.html

简单的observe定义如下

需要监控data的属性值,对象的某个属性被赋值了,就会触发setter,这样就能监听到数据变化。

需要将vm.data[name]属性改为vm[name]

完整代码如下:

<!DOCTYPE html>
<head></head>
<body>
    <div id="app">
        <input type="text" id="a" v-model="text">
        {{text}}
    </div>
    <script type="text/javascript">
      function Compile(node,vm) {
            if(node) {
                this.$frag = this.nodeToFragment(node,vm);
                return this.$frag;
            }
        }

        Compile.prototype = {
            nodeToFragment: function(node,vm) {
                var self = this;
                var frag = document.createDocumentFragment();
                var child;
     
                while(child = node.firstChild) {
                     self.compileElement(child,vm);
                     frag.append(child); // 将所有子节点添加到fragment中
                }
                return frag;
            },compileElement: function(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,name);
                    }
                }
            },}

        function defineReactive (obj,key,val) {
            Object.defineProperty(obj,{
                get: function() {
                    return val;
                },set: function (newVal) {
                    if(newVal === val) return;
                        val = newVal;
                        console.log(val);
                }
            });
        }

        function observe(obj,vm) {
            Object.keys(obj).forEach(function(key) {  //---监听data的每一个属性
                defineReactive(vm,obj[key]);   
            });
        }

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

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

显示效果如下

虽然set方法触发了,但是文本节点{{text}}的内容没有变化,要让绑定的文本节点同步变化,就需要引入订阅者-发布者模式。

例5、订阅发布模式

订阅发布模式(又称观察者模式),定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

发布者发出通知=》主题对象收到通知,并推送给订阅者=》订阅者执行相应操作。

A、首先,要有一个收集订阅者的容器,定义一个Dep作为主题对象。

B、然后,定义订阅者Watcher

C、添加订阅者Watcher到主题对象Dep,发布者发出通知到属性监听里面

D、最后,需要订阅的地方

至此,用DOM操作,实现双向绑定的操作的完整代码如下

<!DOCTYPE html>
<head></head>
<body>
    <div id="app">
        <input type="text" id="a" v-model="text">
        {{text}}
    </div>
    <script type="text/javascript">
      function Compile(node,vm) {
                var self = this;
                var frag = document.createDocumentFragment();
                var child;
       
                while(child = node.firstChild) {
                    self.compileElement(child,vm);
                    frag.append(child); // 将所有子节点添加到fragment中
                }
                return frag;
            },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
                            new Watcher(vm,name,'value');
                        }
                    }
                }
                //节点类型为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,'nodeValue');
                    }
                }
               },}

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

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

        function Watcher(vm,type) {
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.type = type;
            this.update();
            Dep.target = null;
        }
     
        Watcher.prototype = {
            update: function() {
                this.get();
                this.node[this.type] = this.value; // 订阅者执行相应操作
            },// 获取data的属性值
            get: function() {
                this.value = this.vm[this.name]; //触发相应属性的get
            }
        }

        function defineReactive (obj,val) {
            var dep = new Dep();
            Object.defineProperty(obj,{
                get: function() {
                    //添加订阅者watcher到主题对象Dep
                    if(Dep.target) {
                        // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
                        dep.addSub(Dep.target);
                    }
                   return val;
                },set: function (newVal) {
                    if(newVal === val) return;
                        val = newVal;
                        console.log(val);
                        // 作为发布者发出通知
                        dep.notify();
                    }
            });
        }

        function observe(obj,vm) {
            Object.keys(obj).forEach(function(key) {
                defineReactive(vm,obj[key]);
            });
        }
     
       function Vue(options) {
            this.data = options.data;
            var data = this.data;
            observe(data,data: {
                text: 'hello world'
            }
        });
     </script>
</body>
</html>

效果如下:可以双向绑定数据了

总结:Vue.js实现数据双向通信的原理,用几句话简单的概括:

A、通过observe()函数里面Object.defineProperty()监听UI的变化,从而得到数据的变化;再通过compile()函数监听绑定该数据的DOM节点,当数据变化的时候就会通知这些节点更新数据,从而实现UI的变化。

B、observe()函数实现监听UI变化,从而获得新的数据;compile()函数实现监听数据变化,然后将数据传送到绑定该数据的DOM节点上显示。

 

 

 

 

 

 

 

 

 

相关文章

https://segmentfault.com/a/1190000022018995 https://www....
ES6 (ECMAScript 6)中的模块是一个包含 JavaScript 代码的...
from https://mp.weixin.qq.com/s/-rc1lYYlsfx-wR4mQmIIQQ V...
D:\Temp&gt;npm init vite@latest vue3study --temp...
文章浏览阅读1.2k次。最近自己从零撸起的甘特图组件需要子组...
文章浏览阅读3.3k次,点赞3次,收藏16次。静默打印是什么?简...