MVC模式是单向绑定,即model绑定到view,当我们用js代码更新model时,view就会自动更新。
MVVM模式
MVVM模式就是model-view-viewmodel,它实现了view的变动,自动反应在viewmodel,反之亦然。
我对于双向绑定的理解就是用户更新了view,model的数据也自动被更新了。这种情况就是双向绑定。
再说细点就是在单向绑定的基础上给可输入元素(input, textarea)元素添加(change(input)事件)change事件触发,view的状态就被更新了
发布-订阅模式
订阅者和发布者模式,通常用于消息队列中。一般有两种形式来实现消息队列,一是使用生产者和消费者来实现,二是订阅-发布者来实现,其中使用订阅者和发布者实现的消息队列的方式,就会用订阅者模式。
所谓的订阅者,就像我们在日常生活中,订阅报纸一样。我们订阅报纸的时候,通常都得需要再报社或者一些中介机构进行注册。当有新版的报纸发刊的时候,邮递员就需要向订阅该报纸的人,依次发放报纸,因此代码实现该模式,通常需要两个步骤
初始化发布者,订阅者。 订阅者需要注册到发布者,发布者发布消息,依次向订阅者发布消息。
订阅者注册
发布者发布消息
举一个常见的例子,双十一商品打折,只有关注该商品的客户才能收到该商品打折信息。
商品信息类主要成员变量用来存放所有关注该商品的顾客(ArrayList),同时存在发布消息的方法,客户关注商品的方法(即向客户容器中添加客户)等。
1 //商品类 2 class Product{ 3 private ArrayList<Client> clientArrayList = new ArrayList<Client>(); 4 5 public void notify(String info){ 6 //对于所有关注该商品的顾客进行遍历,依次将信息发布出去 7 for(Client client:clientArrayList){ 8 client.receiveInfo(info); 9 } 10 } 11 12 public void register(Client client){ 13 clientArrayList.add(client); 14 } 15 }
客户类,即订阅者,用来接受商品(发布者)的打折信息。
1 class ConcreteClient { 2 3 private String name; 4 5 public ConcreteClient(String name){ 6 this.name = name; 7 } 8 //用来接受发布者发布的消息 9 public void receiveInfo(String info) { 10 System.out.println(this.name + ":收到信息(" + info + ")"); 11 } 12 }
测试
1 @Test 2 public void test(){ 3 Product product = new Product(); 4 5 //a 用户关注商品 6 product.register(new ConcreteClient("a")); 7 //b 用户关注商品 8 product.register(new ConcreteClient("b")); 9 //c 用户关注商品 10 product.register(new ConcreteClient("c")); 11 //d 用户关注商品 12 product.register(new ConcreteClient("d")); 13 14 product.notify("商品编号为D1403121717大减价"); 15 }
运行结果
1 a:收到信息(商品编号为D1403121717大减价);
2 b:收到信息(商品编号为D1403121717大减价);
3 c:收到信息(商品编号为D1403121717大减价);
4 d:收到信息(商品编号为D1403121717大减价);
VUE双向绑定原理
vue数据双向绑定原理就是通过数据劫持结合发布者-订阅者模式的方式来实现的。
我们看一下vue初始化的数据是什么东西
1 var vm = new Vue({ 2 data: { 3 obj: { 4 a: 1 5 } 6 }, 7 created: function () { 8 console.log(this.obj); 9 } 10 });
结果
我们看出a有两个相对应的get和set方法,为什么会多出这两个方法呢?因为vue是通过Object.defineProperty()来实现数据劫持的
Object.defineProperty()是用来做什么?它可以控制一个对象属性的一些特有操作,比如读写权,是否可以枚举
在平时很容易就可以打印一个对象的属性数据:
var Book = {
name: 'vue权威指南'
};
console.log(Book.name); // vue权威指南
如果想要执行console.log(book.name)的同时,直接给书名加个书名号,要怎么处理呢,或者说通过什么监听Book的属性值。这时候,Object.defineProperty()就派上的用场,代码如下
var Book = {}
var name = '';
Object.defineProperty(Book, 'name', {
set: function (value) {
name = value;
console.log('你取了一个书名叫做' + value);
},
get: function () {
return '《' + name + '》'
}
})
Book.name = 'vue权威指南'; // 你取了一个书名叫做vue权威指南
console.log(Book.name); // 《vue权威指南》
我们通过Object.defineProperty()方法设置了对象的name属性,对其get和set进行重写操作,顾名思义,get就是读取name属性这个值触发的函数,set就是在设置name属性这个值触发的函数,所以当执行Book.name='vue权威指南'这个语句时,控制台会打印出“你取了一个书名叫vue权威指南”,紧接着,当读取这个属性时,就会输出“《VUE权威指南》”,因为我们在get函数里面对该值进行加工了,如果这个时候我们执行下面的语句,控制台会输出什么
console.log(Book);
结果
乍一看,是不是跟我们在上面打印vue数据长得有点类似,说明vue确实是通过这种方法来进行数据劫持的
思路分析
实现MVVM主要包括两个方面,数据变化更新视图,视图变化更新数据
关键点在于data如何更新view,因为view更新data实际可以通过事件监听即可,比如input标签监听‘input'事件就可以实现了,所以我们着重分析下,当数据改变如何更新视图。
数据更新的重点是如何知道数据改变了,只要知道数据变了,那么接下来的事就好处理了,如何知道数据变了,其实上文就已经给出答案了,就是通过Object.defineProperty()对属性设置一个set函数,当数据改变了就会触发这个函数,所以我们只要将一些需要更新的方法放在这里就可以实现data更新view了
我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器,用来监听所有属性,如果属性发生变化了,就需要告诉订阅者Watcher看是否需要更新,因为订阅者有很多个,我们需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着还需要一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定的相关函数,从而更新视图。因此接下来我们执行以下三个步骤,实现数据的双向绑定:
1,实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2,实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3,实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
流程如下
1.实现一个Observer
Observer是一个数据监听器,其实现核心方法就是前文所说的Object.defineProperty( )。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行Object.defineProperty( )处理。如下代码,实现了一个Observer。
1 function defineReactive(data, key, val) { 2 observe(val); // 递归遍历所有子属性 3 Object.defineProperty(data, key, { 4 enumerable: true, 5 configurable: true, 6 get: function() { 7 return val; 8 }, 9 set: function(newVal) { 10 val = newVal; 11 console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”'); 12 } 13 }); 14 } 15 16 function observe(data) { 17 if (!data || typeof data !== 'object') { 18 return; 19 } 20 Object.keys(data).forEach(function(key) { 21 defineReactive(data, key, data[key]); 22 }); 23 }; 24 25 var library = { 26 book1: { 27 name: '' 28 }, 29 book2: '' 30 }; 31 observe(library); 32 library.book1.name = 'vue权威指南'; // 属性name已经被监听了,现在值为:“vue权威指南” 33 library.book2 = '没有此书籍'; // 属性book2已经被监听了,现在值为:“没有此书籍”