精华内容
下载资源
问答
  •  vue数据双向绑定如何实现的?实现的具体步骤。      </p>
  • Vue的双向数据绑定是通过数据劫持结合发布者订阅者模式来实现的 vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,...

    Vue中的双向数据绑定是如何实现的

    Vue的双向数据绑定是通过数据劫持结合发布者订阅者模式来实现的

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

    具体步骤:

    第一步: 需要observer的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
    这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

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

    第三步Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
    1、在自身实例化时往属性订阅器(dep)里面添加自己
    2、自身必须有一个update()方法
    3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

    第四步MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

    简单理解
    new Vue的时候,在Observer中通过Object.defineProperty()达到数据劫持,代理所有数据的gettersetter属性,在每次触发setter的时候,都会通过Dep来通知WatcherWatcher作为Observer数据监听器与Compile模板解析器之间的桥梁,当Observer监听到数据发生改变的时候,通过Updater来通知Compile更新视图

    Compile通过Watcher订阅对应数据,绑定更新函数,通过Dep来添加订阅者,达到双向绑定
    在这里插入图片描述

    展开全文
  • 2、缓解好奇心同时了解如何实现双向绑定 为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组处理、数据循环依赖等,也难免存在一些问题,欢迎大家指正。...
  • 实现Vue的双向绑定

    2018-07-07 18:41:00
    之前有讲到过vue实现整体的整体流程,讲到过数据的响应式,是通过Object.defineProperity来实现的,当时只是举了一个小小的例子,那么再真正的vue框架里是如何实现数据的双向绑定呢?是如何将vm.data中的属性通过“v...

    一、概述

    之前有讲到过vue实现整体的整体流程,讲到过数据的响应式,是通过Object.defineProperity来实现的,当时只是举了一个小小的例子,那么再真正的vue框架里是如何实现数据的双向绑定呢?是如何将vm.data中的属性通过“v-model”和“{{}}”绑定到页面上的呢?下面我们先抛弃vue中DOM渲染的机制,自己来动手实现一双向绑定的demo。

    二、实现步骤

    1、html部分

    根据Vue的语法,定义html需要绑定的DOM,如下代码

    2、js部分

    由于直接操作DOM是非常损耗性能的,所以这里我们使用DocumentFragment(以下简称为文档片段),由于createDocumentFragment是在内存中创建的一个虚拟节点对象,所以往文档片段里添加DOM节点是不太消耗性能的;此处我们将app下面的节点都劫持到文档片段中,在文档片段中对DOM进行一些操作,然后将文档片段总体重新插入app容器里面去,而且此处插入到app中的节点都是属于文档片段的子孙节点。代码如下:

     1 // 劫持DOM节点到DocumentFragment中
     2 function nodeToFragment(node) {
     3     var flag = document.createDocumentFragment();
     4     while(node.firstChild) {
     5         flag.appendChild(node.firstChild) // 劫持节点到文档片段中,在此之前对节点进行一些操作; 劫持到一个,对应的DOM容器里会删除掉一个节点
     6     }
     7     return flag 
     8 };
     9 var dom = nodeToFragment(document.getElementById('app'))
    10 document.getElementById('app').apendChild(dom) // 将文档片段重新放入app中

    对于双向绑定的实现,首先我们来创建vue的实例

     1 // 创建Vue对象
     2 function Vue(data) {
     3     var id = data.el;
     4     var ele = document.getElementById(id);
     5     this.data = data.data;
     6     obersve(this.data, this)    // 将vm.data指向vm
     7     var dom = nodeToFragment(ele, this);   // 通过上面的函数劫持DOM节点
     8     ele.appendChild(dom);     // 将文档片段重新放入容器
     9 };
    10 // 实例化Vue对象
    11 var vm = new Vue({
    12     el: 'app',
    13     data: {
    14         text: 'hello world'
    15     }
    16 })

    通过以上代码我们可以看到,实例化Vue对象的时候先是将vm.data指向到了vm,而后是对html节点进行的数据绑定,此处分两步,我们先来看对vm的数据源绑定:

     1 function definevm(vm, key, value) {
     2     Object.defineProperty(vm, key, {
     3         get: function() {
     4             return value
     5         },
     6         set: function(newval) {
     7             value = newval
     8             console.log(value)
     9         }
    10     })
    11 };
    12 // 指定data到vm
    13 function obersve(data, vm) {
    14     for(var key in data) {
    15         definevm(vm, key, data[key]);
    16     }
    17 }
    18 
    19 vm.text = 'MrGao';
    20 console.log(vm.text);  // MrGao

    此处将vm.data的属性指定到vm上,并且实现了对vm的监听,一旦vm的属性发生变化,便会触发其set方法;接下来我们来看下对DOM节点的数据绑定:

     1 // 绑定数据
     2 function compile(node, vm) {
     3     // console.log(node.nodeName)
     4     var reg = /\{\{(.*)\}\}/;    // 匹配{{}}里的内容
     5     if (node.nodeType === 1) {   // 普通DOM节点nodeType为1
     6         var attr = node.attributes  遍历节点属性
     7         for(var i = 0; i < attr.length; i++) {
     8             if (attr[i].nodeName === 'v-model') {
     9                 var name = attr[i].nodeValue;     // 获取绑定的值
    10                 node.addEventListener('keyup', function(e) {
    11                     // console.log(e.target.value)
    12                     vm[name] = e.target.value    //监听input值的变化,重新给vm.text赋值
    13                 })
    14                 node.value = vm[name];
    15                 node.removeAttribute('v-model');
    16             };
    17         };
    18     };
    19     if (node.nodeType === 3) {
    20         if (reg.test(node.nodeValue)) {
    21             var name = RegExp.$1;
    22             name = name.trim();
    23             node.nodeValue = vm[name];          // 将vm.text的值赋给文本节点
    24         }
    25     }
    26 }
    27 // 劫持DOM节点到DocumentFragment中
    28 function nodeToFragment(node, vm) {
    29     var flag = document.createDocumentFragment();
    30     while(node.firstChild) {
    31         compile(node.firstChild, vm);          // 进行数据绑定
    32         flag.appendChild(node.firstChild);     // 劫持节点到文档片段中
    33     }
    34     return flag;
    35 };

    这样一来,我们就可以通过compile方法将vm.text绑定到input节点和下面的文本节点上,并且监听input节点的keyup事件,当input的value发生改变是,将input的值赋给vm.text,如此vm.text的值也改变了,同时会触发对vm的ste函数;但是vm.text的值是改变了,我们应该如何让文本节点的值同样跟随者vm.text的值改变呢?此时我们就可以使用订阅模式(观察者模式)来实现这一功能;那什么是订阅模式呢?

    订阅模式就是好比有一家报社,他每天都要对新的世界大事进行发布,然后报社通知送报员去把发布的新的报纸推送给订阅者,订阅这在拿到报纸后可以获取到新的消息;反映到代码里可以这样理解;当vm.text改变时,触发set方法,然后发布变化的消息,在数据绑定的那里定义订阅者,在定义一个连接两者的“送报员”,每当发布者发布新的消息,订阅者都可以拿到新的消息,代码如下:

     1 // 定义发布订阅
     2 function Dep() {
     3     this.subs = []
     4 }
     5 Dep.prototype = {
     6     addSub: function(sub) {
     7         this.subs.push(sub);
     8     },
     9     notify: function() {
    10         this.subs.forEach(function(sub) {
    11             sub.update();
    12         })
    13     }
    14 };
    15 //  定义观察者
    16 function Watcher (vm, node, name) {
    17     Dep.target = this;   // 发布者和订阅者的桥梁(送报员)
    18     this.name = name;
    19     this.node = node;
    20     this.vm = vm;
    21     this.update();
    22     Dep.target = null;
    23 };
    24 Watcher.prototype = {
    25     update: function() {
    26         this.get();
    27         // console.log(this.node.nodeName)
    28         if (this.node.nodeName === 'INPUT') {
    29             this.node.value = this.value;
    30         } else {
    31             this.node.nodeValue = this.value;
    32         }
    33     },
    34     get: function() {
    35         this.value = this.vm[this.name];
    36     }
    37 }

    此时,发布者和订阅者要分别在数据更新时和数据绑定时进行绑定

     1 // 绑定发布者
     2 function definevm(vm, key, value) {
     3     var dep = new Dep  // 实例化发布者
     4     Object.defineProperty(vm, key, {
     5         get: function() {
     6             if (Dep.target) {
     7                 dep.addSub(Dep.target)  // 为每个属性绑定watcher
     8             }
     9             return value
    10         },
    11         set: function(newval) {
    12             value = newval
    13             console.log(value)
    14             dep.notify();     // 数据改变执行发布
    15         }
    16     })
    17 };
    18 
    19 // 绑定订阅者到节点上面
    20 function compile(node, vm) {
    21     // console.log(node.nodeName)
    22     var reg = /\{\{(.*)\}\}/;
    23     if (node.nodeType === 1) {
    24         var attr = node.attributes
    25         for(var i = 0; i < attr.length; i++) {
    26             if (attr[i].nodeName === 'v-model') {
    27                 var name = attr[i].nodeValue;
    28                 node.addEventListener('keyup', function(e) {
    29                     // console.log(e.target.value)
    30                     vm[name] = e.target.value
    31                 })
    32                 // node.value = vm[name];
    33                 new Watcher(vm, node, name);   // 初始化绑定input节点
    34                  node.removeAttribute('v-model');
    35             };
    36         };
    37     };
    38     if (node.nodeType === 3) {
    39         if (reg.test(node.nodeValue)) {
    40             var name = RegExp.$1;
    41             name = name.trim();
    42             // node.nodeValue = vm[name];
    43             new Watcher(vm, node, name);   // 文本节点绑定订阅者
    44         }
    45     }
    46 }

    到这里vue的双绑定就实现了,此文仅为实现最简单的双向绑定,一些其它复杂的条件都没有考虑在内,为理想状态下,如有纰漏还望指正,下面附上完整代码

      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4     <meta charset="UTF-8">
      5     <title>Vue</title>
      6 </head>
      7 <body>
      8     <div id="app">
      9         <input type="text" id="a" v-model="text">
     10         {{text}}
     11     </div>
     12 </body>
     13 <script>
     14     // 定义发布订阅
     15     function Dep() {
     16         this.subs = []
     17     }
     18     Dep.prototype = {
     19         addSub: function(sub) {
     20             this.subs.push(sub);
     21         },
     22         notify: function() {
     23             this.subs.forEach(function(sub) {
     24                 sub.update();
     25             })
     26         }
     27     };
     28     //  定义观察者
     29     function Watcher (vm, node, name) {
     30         Dep.target = this;
     31         this.name = name;
     32         this.node = node;
     33         this.vm = vm;
     34         this.update();
     35         Dep.target = null;
     36     };
     37     Watcher.prototype = {
     38         update: function() {
     39             this.get();
     40             // console.log(this.node.nodeName)
     41             if (this.node.nodeName === 'INPUT') {
     42                 this.node.value = this.value;
     43             } else {
     44                 this.node.nodeValue = this.value;
     45             }
     46         },
     47         get: function() {
     48             this.value = this.vm[this.name];
     49         }
     50     }
     51     // 绑定数据
     52     function compile(node, vm) {
     53         // console.log(node.nodeName)
     54         var reg = /\{\{(.*)\}\}/;
     55         if (node.nodeType === 1) {
     56             var attr = node.attributes
     57             for(var i = 0; i < attr.length; i++) {
     58                 if (attr[i].nodeName === 'v-model') {
     59                     var name = attr[i].nodeValue;
     60                     node.addEventListener('keyup', function(e) {
     61                         // console.log(e.target.value)
     62                         vm[name] = e.target.value
     63                     })
     64                     // node.value = vm[name];
     65                     new Watcher(vm, node, name);
     66                     node.removeAttribute('v-model');
     67                 };
     68             };
     69         };
     70         if (node.nodeType === 3) {
     71             if (reg.test(node.nodeValue)) {
     72                 var name = RegExp.$1;
     73                 name = name.trim();
     74                 // node.nodeValue = vm[name];
     75                 new Watcher(vm, node, name);
     76             }
     77         }
     78     }
     79     // 劫持DOM节点到DocumentFragment中
     80     function nodeToFragment(node, vm) {
     81         var flag = document.createDocumentFragment();
     82         while(node.firstChild) {
     83             // console.log(node.firstChild)
     84             compile(node.firstChild, vm)
     85             flag.appendChild(node.firstChild) // 劫持节点到文档片段中
     86         }
     87         return flag
     88     };
     89     function definevm(vm, key, value) {
     90         var dep = new Dep
     91         Object.defineProperty(vm, key, {
     92             get: function() {
     93                 if (Dep.target) {
     94                     dep.addSub(Dep.target)
     95                 }
     96                 return value
     97             },
     98             set: function(newval) {
     99                 value = newval
    100                 console.log(value)
    101                 dep.notify();
    102             }
    103         })
    104     };
    105     // 指定data到vm
    106     function obersve(data, vm) {
    107         for(var key in data) {
    108             definevm(vm, key, data[key]);
    109         }
    110     }
    111     // 创建Vue类
    112     function Vue (options) {
    113         this.data = options.data;
    114         var id = options.el;
    115         var ele = document.getElementById(id);
    116 
    117         // 将data的数据指向vm
    118         obersve(this.data, this);
    119         // 存DOM到文档片段
    120         var dom = nodeToFragment(ele, this);
    121         // 编译完成将DOM返回挂在容器中
    122         ele.appendChild(dom);
    123     };
    124     // 创建Vue实例
    125     var vm = new Vue({
    126         el: 'app',
    127         data: {
    128             text: 'hello world'
    129         }
    130     })
    131 </script>
    132 </html>

    参考文章:https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

    转载于:https://www.cnblogs.com/gaosong-shuhong/p/9278069.html

    展开全文
  • Vue双向绑定的实现

    2019-09-25 23:10:23
    今天突然有人问我,Vue的双向绑定是怎么回事,其实接触过Vue的人应该多少会了解一些,不过突然让我用语言描述一下,还真不知道该怎么描述好了,所以我整理了一下语言,来进行一下简单的说明。 这个双向绑定有什么...

     

    今天突然有人问我,Vue的双向绑定是怎么回事,其实接触过Vue的人应该多少会了解一些,不过突然让我用语言描述一下,还真不知道该怎么描述好了,所以我整理了一下语言,来进行一下简单的说明。

     

    这个双向绑定有什么效果就不多说了,用过的都知道,那么它是由谁来提供的呢,它又是如何实现的呢,对于这两个问题,我们一一来看,首先来说Vue中的双向绑定是由MVVM中的VM提供的,那么这个MVVM又是什么,和MVC有什么关系吗,先来解释一下MVC和MVVM。

     

    MVC 是后端的分层开发概念,目的是为了解耦提高开发效率,将项目结构分成了3个大模块,即M(Model)- V(View)- C(Controller),其中M(Model)是指数据层,这层的主要作用就是和数据库打交道,对数据进行增删改查(CRUD)的操作,V(View)通常指的就是视图层,多指网页,这层的主要作用就是数据的展示,C(Controller)是指控制层,这层的主要作用是进行业务处理和路由分发。

     

    MVVM是前端的分层开发概念,思想和MVC类似,把每个页面分成了3个大模块,主要是为了开发更加方便,其中M(Model)指的是从后端服务器获取到的数据,V(View)通常指的是HTML结构,VM(View-Model)扮演的是V(View)和M(Model)之间的调度者,也是MVVM模式的核心,因为VM(View-Model)提供了数据的双向绑定。

     

    了解了MVC和MVVM之后,也知道了VM是MVVM的核心,并且清楚了双向绑定是由VM来提供的,接下来,我来简单说明一下整个的执行过程,让大家有个更清晰的认识。首先,客户端会通过浏览器打开网页,此时的网页就是我们MVVM中的V,V发现自己没有内容,只有结构,所以此时V先向VM发送请求,然后由VM再向M发送请求,M收到请求之后,再向后端服务器发送请求,后端收到请求后,将数据回传给M,此时,M再将数据返给VM,由VM交给V进行渲染,最终呈现到客户端。由此可以了解,VM的作用以及重要性。

     

    为了更加深刻的理解体会双向绑定和实现原理,我们来自己实现一个简易的双向绑定。通过官方文档我们知道,VM实现双向绑定的核心在于:

    Object.defineProperty(obj, prop, desc)

    它的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,再来看这张表

     

    从这个表中我们可以看到很多属性,因为是一个简易的双向绑定版本,我们这里暂时只会用到一个属性,那就是set,通过名字就知道是设置值用的,那么我们就结合语法先来简单的用一下看看

    <p>输入:</p>
    <input type="text" id="text-ipt" />
    <p>显示:</p>
    <p id="text-box"></p>
    
    <script type="text/javascript">
        var o = {};
        Object.defineProperty(o, 'test', {
            set: function(newValue){
                // 为P标签和文本框同时赋值,以便实现双向绑定效果
                document.getElementById('text-ipt').value = newValue;
                document.getElementById('text-box').innerText = newValue;
            }
        });
    
        // 我们需要调用一下这个对象o的test属性才可以
        document.getElementById('text-ipt').addEventListener('input', function(event){
    	    o.test= event.target.value;    // 此时相当于调用了对象o的set方法来修改值
        });
    </script>

    运行结果如下,你每次在文本框输入时,p标签中的数据都会实时更新

    至此,一个简易的双向绑定就已经完成了。

     

    其实还有很多地方并不是那么准确和完善,但时间和水平有限,随着自己对Vue的深入了解,后期还会继续完善和更新,也欢迎前辈们补充指正。

    展开全文
  • vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的?说白了就是通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的...

    1.vue的实现原理

    vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的?说白了就是通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情.

    复制代码

    var vm = new Vue({
        data: {
            test : {
                a: 1
            }
        },
        created: function () {
            console.log(this.test);
        }
    });

    get和set.为什么会有这两个方法呢,这正是vue通过Object.defineProperty()进行数据劫持的.

    Object.defineProperty()这个方法是做什么的呢?文档上是这样说的

     

     简单的说,他可以控制一个对象属性的一些特有操作,比如读写权,是否可枚举,这里我们主要研究它的get和set方法

    我们可以很轻松的打印出一个对象的属性数据:

    var Book = {
      name: '人性的弱点'
    };
    console.log(Book.name);  // 人性的弱点

    复制代码

    //在console.log(book.name)同时,直接给书加一个书号
    var Book = {};
    var name = '';
    Object.defineProperty(Book,'name',{
        set:function(value) {
            name = value;
            console.log('你取了一个书名叫:'+value);
        },
        get:function() {
            console.log('get方法被监听到');
            return '<'+name+'>';
        }
    });
    Book.name = '人性的弱点';  //你取了一个书名叫:人性的弱点
    console.log(Book.name); //<人性的弱点>

    通过Object.defineProperty( )这个方法设置了Book对象的name属性,对其get和set方法进行重写操作,get方法在获得name属性时被调用,set方法在设置name属性时被触发.所以在执行Book.name='人性的弱点' 这个语句时调用set方法,输出你取了一个书名叫:人性的弱点.当调用console.log(Book.name)时触发get方法,输出<人性的弱点>

    订阅者和发布者模式,通常用于消息队列中.一般有两种形式来实现消息队列,一是使用生产者和消费者来实现,二是使用订阅者-发布者模式来实现,其中订阅者和发布者实现消息队列的方式,就会用订阅者模式.

    所谓的订阅者,就像我们在日常生活中,订阅报纸一样。我们订阅报纸的时候,通常都得需要在报社或者一些中介机构进行注册。当有新版的报纸发刊的时候,邮递员就需要向订阅该报纸的人,依次发放报纸。

    所有如果用代码实现该模式,需要进行两个步骤:

    1、初始化发布者、订阅者。
    2、订阅者需要注册到发布者,发布者发布消息时,依次向订阅者发布消息。

    那么接下来我们将通过vue原理实现一个简单的mvvm双向绑定的demo

    思路分析

    要想实现mvvm,主要包含两个方面,视图变化更新数据,数据变化更新视图.

    view变化更新data其实可以通过事件监听实现,比如input标签监听input事件,所有我们着重分析data变化更新view.

    data变化更新view的重点是如何知道view什么时候变化了,只要知道什么时候view变化了,那么接下来的就好处理了.这个时候我们上文提到的Object.defineProperty( )就起作用了.通过Object.defineProperty( )对属性设置一个set函数,当属性变化时就会触发这个函数,所以我们只需要将一些更新的方法放在set函数中就可以实现data变化更新view了.

    实现过程

    我们已经知道如何实现数据的双向绑定了, 那么首先要对数据进行劫持监听,所以我们首先要设置一个监听器Observer,用来监听所有的属性,当属性变化时,就需要通知订阅者Watcher,看是否需要更新.因为属性可能是多个,所以会有多个订阅者,故我们需要一个消息订阅器Dep来专门收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理.以为在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图.

    整理上面的思路,我们需要实现三个步骤,来完成双向绑定:

    1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
    
    2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
    
    3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

    1.实现一个监听器Observer

    数据监听器的核心方法就是Object.defineProperty( ),通过遍历循环对所有属性值进行监听,并对其进行Object.defineProperty( )处理,那么代码可以这样写:

    复制代码
    //对所有属性都要蒋婷,递归遍历所有属性
    function defineReactive(data,key,val) {
        observe(val);  //递归遍历所有的属性
        Object.defineProperty(data,key,{
            enumerable:true,         //当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。
            configurable:true,       //当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中
            get:function() {
                return val;
            },
            set:function(newVal) {
                val = newVal;
                console.log('属性'+key+'已经被监听,现在值为:"'+newVal.toString()+'"');
            }
        })
    }
    
    function observe(data) {
        if(!data || typeof data !== 'object') {
            return;
        }
        Object.keys(data).forEach(function(key){
            defineReactive(data,key,data[key]);
        });
    }
    
    var library = {
        book1: {
            name: ''
        },
        book2: ''
    };
    observe(library);
    library.book1.name = 'vue权威指南'; // 属性name已经被监听了,现在值为:“vue权威指南”
    library.book2 = '没有此书籍';  // 属性book2已经被监听了,现在值为:“没有此书籍”
    复制代码

    通过observe()方法进行遍历向下找到所有的属性,并通过defineReactive()方法进行数据劫持监听.

    在上面的思路中,我们需要一个可以容纳消息订阅者的消息订阅器Dep,订阅器主要收集消息订阅者,然后在属性变化时执行相应订阅者的更新函数,那么消息订阅器Dep需要有一个容器,用来存放消息订阅者.我们将上面的监听器Observer稍微修改一下:

    function defineReactive(data,key,val) {
        observe(val);
        var dep = new Dep();
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                if (是否需要添加订阅者) {    //Watcher初始化触发
                    dep.addSub(watcher); // 在这里添加一个订阅者
                }
                return val;
            },
            set: function(newVal) {
                if (val === newVal) {
                    return;
                }
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
                dep.notify(); // 如果数据变化,通知所有订阅者
            }
        });
    }
    
    function observe(data) {
        if(!data || typeof data !== 'object') {
            return;
        }
        Object.keys(data).forEach(function(key){
            defineReactive(data,key,data[key]);
        });
    }
    
    function Dep() {
        this.subs = [];
    }
    
    //prototype 属性使您有能力向对象添加属性和方法
    //prototype这个属性只有函数对象才有,具体的说就是构造函数具有.只要你声明定义了一个函数对象,这个prototype就会存在
    //对象实例是没有这个属性
    Dep.prototype = {                        
        addSub:function(sub) {
            this.subs.push(sub);
        },
        notify:function() {
            this.subs.forEach(function(sub) {
                sub.update();        //通知每个订阅者检查更新
            })
        }
    }
    Dep.target = null;

    在代码中,我们将订阅器Dep添加一个订阅者设计在get里面,这是为了让Watcher在初始化时触发,因此判断是否需要需要添加订阅者,至于具体实现的方法,我们在下文中深究.在set方法中,如果函数变化,就会通知所有的订阅者,订阅者们将会执行相对应的更新函数,到目前为止,一个比较完善的Observer已经成型了,下面我们要写订阅者Watcher.

    2.实现订阅者Watcher

    根据我们的思路,订阅者Wahcher在初始化时要将自己添加到订阅器Dep中,那么如何进行添加呢?

    我们已经知道监听器Observer是在get函数中执行了添加订阅者的操作的,所以我们只需要在订阅者Watcher在初始化时触发相对应的get函数来执行添加订阅者的操作即可.那么怎么触发对应的get函数呢?我们只需要获取对应的属性值,就可以通过Object.defineProperty( )触发对应的get了.

    在这里需要注意一个细节,我们只需要在订阅者初始化时才执行添加订阅者,所以我们需要一个判断,在Dep.target上缓存一下订阅者,添加成功后去除就行了,代码如下:

    function Watcher(vm,exp,cb) {
        this.vm = vm;    //指向SelfVue的作用域
        this.exp = exp;  //绑定属性的key值
        this.cb = cb;    //闭包
        this.value = this.get();
    }
    
    Watcher.prototype = {
        update:function() {
            this.run();
        },
        run:function() {
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if(value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm,value,oldVal);
            }
        },
        get:function() {
            Dep.target = this;                   // 缓存自己
            var value = this.vm.data[this.exp];  // 强制执行监听器里的get函数
            Dep.target = null;                   // 释放自己
            return value;
        }
    }

    这个时候我们需要对监听器Observer中的defineReactive()做稍微的调整:

    复制代码
    function defineReactive(data,key,val) {
        observe(val);
        var dep = new Dep();
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                if(Dep.target) {   //判断是否需要添加订阅者
                     dep.addSub(Dep.target);
                }
                return val;
            },
            set: function(newVal) {
                if (val === newVal) {
                    return;
                }
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
                dep.notify(); // 如果数据变化,通知所有订阅者
            }
        });
    }
    复制代码

     

    到目前为止,一个简易版的Watcher已经成型了,我们只需要将订阅者Watcher和监听器Observer关联起来,就可以实现一个简单的双向绑定.因为这里还没有设计指令解析器,所以对于模板数据我们都进行写死处理,假设模板上有一个节点元素,且id为'name',并且双向绑定的绑定变量也是'name',且是通过两个大双括号包起来(暂时没有什么用处),模板代码如下:

    <body>
        <h1 id="name">{{name}}</h1>
    </body>

     

    我们需要定义一个SelfVue类,来实现observer和watcher的关联,代码如下:

    复制代码
    //将Observer和Watcher关联起来
    function SelfVue(data,el,exp) {
        this.data = data;
        observe(data);
        el.innerHTML = this.data[exp];
        new Watcher(this,exp,function(value) {
            el.innerHTML = value;
        });
        return this;
    }
    复制代码

     

    然后在页面上new一个SelfVue,就可以实现双向绑定了:

    复制代码
    <body>
        <h1 id="name"{{name}}></h1>
    </body>
    
    <script src="../js/observer.js"></script>
    <script src="../js/Watcher.js"></script>
    <script src="../js/SelfVue.js"></script>
    
    <script>
         var ele = document.querySelector('#name');
         var selfVue = new SelfVue({
             name:'hello world'
         },ele,'name');
    
         window.setTimeout(function() {
             console.log('name值改变了');
             selfVue.name = 'byebye world';
         },2000);
    </script>

    这时我们打开页面,显示的是'hello world',2s后变成了'byebye world',一个简单的双向绑定实现了.

    对比vue,我们发现了有一个问题,我们在为属性赋值的时候形式是: '  selfVue.data.name = 'byebye world'  ',而我们理想的形式是:'  selfVue.name = 'byebye world'  ',那么怎么实现这种形式呢,只需要在new SelfVue时做一个代理处理,让访问SelfVue的属性代理为访问selfVue.data的属性,原理还是使用Object.defineProperty( )对属性在包装一层.代码如下:

    复制代码
    function SelfVue(data,el,exp) {
        var self = this;
        this.data = data;
        //Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
        Object.keys(data).forEach(function(key) {
            self.proxyKeys(key);     //绑定代理属性
        });
        observe(data);
        el.innerHTML = this.data[exp];   // 初始化模板数据的值
        new Watcher(this,exp,function(value) {
            el.innerHTML = value;
        });
        return this;
    }
    
    SelfVue.prototype = {
        proxyKeys:function(key) {
            var self = this;
            Object.defineProperty(this,key,{
                enumerable:false,
                configurable:true,
                get:function proxyGetter() {
                    return self.data[key];
                },
                set:function proxySetter(newVal) {
                    self.data[key] = newVal;
                } 
            });
        }
    }

    这样我们就可以用理想的形式改变模板数据了.

    3.实现指令解析器Compile

    再上面的双向绑定demo中,我们发现整个过程都没有解析dom节点,而是固定某个节点进行替换数据,所以接下来我们要实现一个解析器Compile来解析和绑定工作,分析解析器的作用,实现步骤如下:

    1.解析模板指令,并替换模板数据,初始化视图
    
    2.将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

    为了解析模板,首先要获得dom元素,然后对含有dom元素上含有指令的节点进行处理,这个过程对dom元素的操作比较繁琐,所以我们可以先建一个fragment片段,将需要解析的dom元素存到fragment片段中在做处理:

    复制代码
    nodeToFragment:function(el) {
            var fragment = document.createDocumentFragment();   //createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
            var child = el.firstChild;
            while(child) {
                // 将Dom元素移入fragment中
                fragment.appendChild(child);
                child = el.firstChild;
            }
            return fragment;
        }
    复制代码

     

    接下来需要遍历所有节点,对含有指令的节点进行特殊的处理,这里我们先处理最简单的情况,只对带有 '{{变量}}' 这种形式的指令进行处理,代码如下:

    复制代码
    //遍历各个节点,对含有相关指定的节点进行特殊处理
        compileElement:function(el) {
            var childNodes = el.childNodes;   //childNodes属性返回节点的子节点集合,以 NodeList 对象。
            var self = this;
            //slice() 方法可从已有的数组中返回选定的元素。
            [].slice.call(childNodes).forEach(function(node) {
                var reg = /\{\{(.*)\}\}/;
                var text = node.textContent;  //textContent 属性设置或返回指定节点的文本内容
                if(self.isTextNode(node) && reg.test(text)) {      //判断是否符合{{}}的指令
                    //exec() 方法用于检索字符串中的正则表达式的匹配。
                    //返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。
                    self.compileText(node,reg.exec(text)[1]);
                }
                if(node.childNodes && node.childNodes.length) {
                    self.compileElement(node);    //继续递归遍历子节点
                }
            });
        },
        compileText:function(node,exp) {
            var self = this;
            var initText = this.vm[exp];
            this.updateText(node,initText);    // 将初始化的数据初始化到视图中
            new Watcher(this.vm,exp,function(value) {
                self.updateText(node,value);
            });
    
        },
        updateText:function(node,value) {
            node.textContent = typeof value == 'undefined' ? '': value;
        },
    复制代码

     

    获取到最外层节点后,调用compileElement函数,对所有子节点进行判断,如果节点是文本节点且匹配{{}}这种形式指令的节点就开始进行编译处理,编译处理首先需要初始化视图数据,对应上面所说的步骤1,接下去需要生成一个并绑定更新函数的订阅器,对应上面所说的步骤2。这样就完成指令的解析、初始化、编译三个过程,一个解析器Compile也就可以正常的工作了。

    为了将解析器Compile与监听器Observer和订阅者Watcher关联起来,我们需要再修改一下类SelfVue函数:

    复制代码
    function SelfVue(options) {
        var self = this;
        this.vm = this;
        this.data = options.data;
        Object.keys(this.data).forEach(function(key) {
            self.proxyKeys(key);     //绑定代理属性
        });
        observe(options.data);
        new Compile(options.el,this.vm);
        return this;
    }
    复制代码

     

    更改后,我们就不要像之前通过传入固定的元素值进行双向绑定了,可以随便命名各种变量进行双向绑定了:

    复制代码
    <body>
        <div id="app">
            <h1>{{title}}</h1>
            <h2>{{name}}</h2>
            <h3>{{content}}</h3>
        </div>
    </body>
    <script src="../js/observer2.js"></script>
    <script src="../js/Watcher1.js"></script>
    <script src="../js/compile1.js"></script>
    <script src="../js/index3.js"></script>
    
    
    <script>
        var selfVue = new SelfVue({
            el:'#app',
            data:{
                title:'aaa',
                name:'bbb',
                content:'ccc'
            }
        });
        window.setTimeout(function() {
            selfVue.title = 'ddd';
            selfVue.name = 'eee';
            selfVue.content = 'fff'
        },2000);
    </script>
    复制代码

     

    到这里,一个数据双向绑定功能已经基本完成了,接下去就是需要完善更多指令的解析编译,在哪里进行更多指令的处理呢?答案很明显,只要在上文说的compileElement函数加上对其他指令节点进行判断,然后遍历其所有属性,看是否有匹配的指令的属性,如果有的话,就对其进行解析编译。这里我们再添加一个v-model指令和事件指令的解析编译,对于这些节点我们使用函数compile进行解析处理:
    复制代码
    compile:function(node) {
            var nodeAttrs = node.attributes;   //attributes 属性返回指定节点的属性集合,即 NamedNodeMap。
            var self = this;
            //Array.prototype属性表示Array构造函数的原型,并允许为所有Array对象添加新的属性和方法。
            //Array.prototype本身就是一个Array
            Array.prototype.forEach.call(nodeAttrs,function(attr) {
                var attrName = attr.name;      //添加事件的方法名和前缀:v-on:click="onClick" ,则attrName = 'v-on:click' id="app" attrname= 'id'
                if(self.isDirective(attrName)) {     
                    var exp = attr.value;      //添加事件的方法名和前缀:v-on:click="onClick" ,exp = 'onClick'
    
                    //substring() 方法用于提取字符串中介于两个指定下标之间的字符。返回值为一个新的字符串
                    //dir = 'on:click'
                    var dir = attrName.substring(2);  
                    if(self.isEventDirective(dir)) {   //事件指令
                        self.compileEvent(node,self.vm,exp,dir);
                    }else {          //v-model指令
                         self.compileModel(node,self.vm,exp,dir);
                    }
    
                    node.removeAttribute(attrName);
                }
            });
        }

    上面的compile函数是挂载Compile原型上的,它首先遍历所有节点属性,然后再判断属性是否是指令属性,如果是的话再区分是哪种指令,再进行相应的处理.

    最后我们再次改造一下SelfVue,是它的格式看上去更像vue:

    复制代码
    function SelfVue(options) {
        var self = this;
        this.data = options.data;
        this.methods = options.methods;
        Object.keys(this.data).forEach(function(key) {
            self.proxyKeys(key);    
        });
        observe(options.data);
        new Compile(options.el,this);
        options.mounted.call(this);
    }
    复制代码

     

    测试一下:

    复制代码
    <body>
        <div id="app">
                <h2>{{title}}</h2>
                <input v-model="name">
                <h1>{{name}}</h1>
                <button v-on:click="clickMe">click me!</button>
        </div>
    </body>
    
    <script src="../js/observer3.js"></script>
    <script src="../js/Watcher1.js"></script>
    <script src="../js/compile2.js"></script>
    <script src="../js/index4.js"></script>
    <script>
        new SelfVue({
            el: '#app',
            data: {
                title: 'hello world',
                name: 'canfoo'
            },
            methods: {
                clickMe: function () {
                    this.title = 'hello world';
                }
            },
            mounted: function () {
                window.setTimeout(() => {
                    this.title = '你好';
                }, 1000);
            }
        });
    </script>
    复制代码

     

    效果如下:

     

    到目前为止,我们简易版的demo已经成功了,通过上面这个例子,我们可以更加深刻的理解vue的一些机制,比如双向绑定,声明式渲染等.

     

    转载于:https://www.cnblogs.com/zhouyideboke/p/9626804.html

    展开全文
  • vue实现双向绑定mvvm

    2018-11-06 23:04:00
    1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环...
  • vue在使用过程中如此便捷就主要源于它的双向数据流,那么它是如何实现的?用简短的代码演示一下vue双向数据绑定的原理便一目了然: 用Object的defindProperty方法。基于访问器属性get set实现。 <input type=...
  • 主要介绍了Vue 如何使用props、emit实现自定义双向绑定的实现,文中通过示例代码介绍非常详细,对大家学习或者工作具有一定参考学习价值,需要朋友们下面随着小编来一起学习学习吧
  • 都知道vue中实现了数据和视图的双向绑定,但具体是如何实现的呢? 今天就说说 我阅读的vue2中的代码实现的个人所得,如果说错了,欢迎指正。 注:我阅读的vue2代码的版本是v2.2.6,且都是以单文件的方式展示列子, ...
  • 这里写自定义目录标题欢迎使用Markdown编辑器新改变功能快捷键合理创建标题,有助于目录生成如何改变文本样式插入链接与图片如何插入一段漂亮代码片生成一个适合你列表创建一个表格设定内容居中、居左、...
  • vue的双向绑定指的是数据变化更新视图,视图变化更新数据。 视图变化更新数据一般就采用事件监听的方式就可以了,数据变化更新视图就需要涉及响应式原理。 vue2.x的响应式原理的基础是Object.defineProperty属性。...
  • 提到了vue实现的基本实现原理:Object.defineProperty() -数据劫持 和 发布订阅者模式(观察者),下面讲的就是数据劫持在代码中的具体实现。 1.先看如何调用 new一个对象,传入我们的参数,这个Myvue ,做了啥? ...
  • 先来简单介绍一下双向绑定,它由两个单向绑定组成: 模型 —> 视图数据绑定; 视图 —> 模型事件绑定。...而在表单中,通过使用内置v-model指令,我们可以轻松地实现双向绑定,比如<input v-mo...
  • Vue双向数据绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变换,试图变化,数据也随之发生变化。 核心:**Object.defineProperty()**方法 ...
  • 思路分析 实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据: ...数据更新视图重点是如何知道数据变了,只要知道数据变了,那么接下去事都好处理。如何知道数据变了,其实上文我们...
  • 1、了解vue的双向数据绑定原理以及核心代码模块 2、缓解好奇心的同时了解如何实现双向绑定 为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环...
  • vue最核心两个功能,一是响应式数据绑定系统,二是组件系统。 实现双向数据绑定: a、实现一个监听器Observer,用来劫持并监听所有属性,如果有变动,就通知订阅者。 b、实现一个订阅者Watcher。每个Watcher都...
  • Vue的双向绑定原理

    2020-03-18 23:01:55
    vue的数据绑定是通过数据劫持结合发布者和订阅者模式来实现的,那么vue是如何进行数据劫持的。 结果: 从上图可以看到属性a有两个相应的get和set方法,为什么会多出这两个方法呢?那是因为Vue是通过object....
  • vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的? 即就是通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情...
  • Compile解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应节点绑定更新函数 watcher负责数据监听,当数据发生改变通知订阅者,调用视图更新函数更新视图 ...
  • vue中最常见的属v-model这个数据双向绑定了,很好奇它是如何实现的呢?尝试着用原生的JS去实现一下。 首先大致学习了解下Object.defineProperty()这个东东吧! * Object.defineProperty() * 对对象的属性进行 定义...
  • 如何实现双向绑定,上图流程显示, (1)需要用Observer来监听属性变化 (2)当属性变化时候,需要通知订阅者Watcher是否需要更新,此时订阅者可能具有多个,因此需要订阅器Dep来专门接收这些订阅者,并统一管理 (3)...
  • Vue如何实现数据双向绑定的Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据 Vue 主要通过以下 4 个步骤来实现数据双向绑定的: 实现⼀个数据监听器 Observer: 对数据对象进⾏遍历,能够对数据...
  • 父子组件之间的双向绑定非常简单,但是很多人因为是从Vue 2+开始使用的,可能不知道如何双向绑定,接下来通过本文给大家介绍Vue父子组件双向绑定传值的实现方法,需要的朋友可以参考下
  • Vue数据双向绑定的原理

    千次阅读 2018-03-27 17:05:31
    vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的双向绑定就是视图上的变化能够反映到数据上,数据上的变化也能反映到视图上。如下图所示: 关键点在于data如何...
  • Vue实现双向数据绑定

    2021-01-19 16:19:42
    Vue如何实现双向数据绑定的呢?答案是前端数据劫持。其通过Object.defineProperty()方法,这个方法可以设置getter和setter函数,在setter函数中,就可以监听到数据变化,从而更新绑定的元素值。 实现对象属性...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 607
精华内容 242
关键字:

vue的双向绑定如何实现的

vue 订阅