简介
自从Angularjs火起来之后,双向绑定经常被提及。双向绑定概念其实很简单,就是视图(View)的变化能实时让数据模型(Model)发生变化,而数据的变化也能实时更新到视图层。我们所说的单向数据绑定就是从数据到视图这一方向的关系。
对于数据双向绑定,我们需要考虑的问题如下:
- 如何监听页面View的变化?
- 如何将View的变化更新到Model?
- 如何监听Model的变化?
- 如何将Model的更新到View?
视图层的变化主要就是表单控件的用户输入行为造成的,比如input,select,textarea等。那么我们只需要监听一些事件,比如keypress,keydown,keyup,change。然后在事件回调函数中,将变化的值更新到Model中。当然同时,由于Model发生了变化,我们得再次更新一下View。
而Model的变化监听方式可以有多种,主要有以下几种: 发布订阅模式(Backbone),数据劫持(VueJS,AvalonJS),数据脏检查(Angularjs,RegularJS), View抽象的脏检查(ReactJS)。最后一个我们暂时不讨论,下面具体对前面的三种类型进行分析。
发布订阅模式
发布订阅模式也称为观察者模式。直观地说,就是有一家报社和很多用户,报社就是发布者,用户就是订阅者。每当报社有新的报纸时,由于订阅者订阅了报纸,他们能第一时间收到新的报纸。当然订阅者也可以取消订阅。
我的理解,原生JS中的事件就是一种观察者模式。比如鼠标的点击事件,只要它点击了,以addEventListener方式订阅它的回调函数就会第一时间收到通知。除了现成的事件,JS的创建自定义事件看起来更直观。
扯远了,我们回到数据双向绑定的主题上来。在数据发生变化的时候,我们发布一个叫‘model-update’的事件。类似,当视图发生变化的时候,我们发布一个叫‘ui-update’的事件。那么,在这些事件发生时想要做什么动作只要让它去订阅这些事件即可。下面是简单的实现,首先定义一个发布订阅者对象pubSub。
每次更新数据用updateData函数,这个函数执行了赋值操作之后会发布‘model-update’事件,这样就手动地解决了数据到视图这方向的更新问题了。
数据劫持
ES5中对象的属性有了属性描述符,可以用以下的方式去定义对象的属性。
除此之外,还可以用getter,setter的方式赋值。当存在getter,setter函数时,属性的赋值操作会触发setter函数的执行,获取操作会触发getter函数的执行。按行业上的术语来说,这样的方法称之为数据劫持。举个栗子。
有了以上的方法之后,我们不难知道,当数据(对象)发生变化,只要在setter函数中发布就好。结合发布订阅者模式,将手动更新数据的updateData函数变成赋值操作,对象会自动执行setter函数,然后就能发布‘model-update’事件了。
想想还有复杂的问题,如果某个属性的值还是对象,怎么办?
我感觉自己都要被自己傻到了。定义data.name的时候还是规规则则地用上面的方法定义就好了。O(∩_∩)O哈!
data.name = {};
defineProperty(data.name, 'English ', "Claire_Yecao");
data.name.English = '小小芬';
还有问题,要是数据的类型是数组,而用push,shift等方法去操作数组,怎么办?
//将数据变成数组
data.name =[ 'Claire_Yecao' ]; // "监听到数据发生了变化"
data.name.push('小小芬 '); // 此时不会发生数据劫持
对于数组,我们针对数组的一些方法进行改写,使得它也能发生劫持。
var arrProto = Object.create(Array.prototype);
['shift','unshift','push','pop','slice','splice'].forEach(function(method){
Object.defineProperty(arrProto, method,{
value: function(){
var result = Array.prototype[method].apply(this, arguments);
console.log('检测数据发生变化');
return result;
}
})
})
var b = [];
b.__proto__ = arrProto;
b.push(1); // 1 '检测数据发生变化'
脏检查
AngularJS的数据双向绑定是基于数据的脏检查机制的。大体意思上来说,就是记录所有变量的当前值,当发生某些操作之后,通过$apply或者$digest进入脏检查环节。对比最近的一次值和现在的值是否一致,不一致则实现页面的更新,然后再执行一次直到数据不再发生变化。
以我的理解稍微详细地说,首先angularJS将它自定义的html页面转化为正常的dom,相对来说就是要解析那些angularJS专有的指令。页面上的指令有compile和link阶段,compile的时候搜索匹配,然后执行指令定义时写的compile函数,link阶段将那些变量插入watch队列。触发脏检查时全部遍历一次watch队列,实现视图的更新。
那么,什么时候会触发脏检查呢?据说有以下几种情况。
- DOM事件,譬如用户输入文本,点击按钮(ng-click)等
- XHR响应事件 ($http)
- 浏览器Location变更事件 ($location)
- Timer事件($timeout, $interval)
- 执行$digest()或$apply()
最后一种情况应该是统一的入口,只不过前几种情况会自动调用这个入口而已。其他情况下,用户需要手动进入脏检查的话,就要执行$digest()或$apply()了。
下面是简单的代码实现。
var watchList = [];
//对于每个需要监听的item
watchItem = {
last: item.value,
get: function(){
return data[attr];
},
callback: function(newVal, oldVal){
console.log("数据发生变化,从 " + oldVal + " 变到 " + newVal);
}
}
watchList.push(watchItem);
//脏检查的入口: Data -> View
function apply(){
var dirty = false;
do{
watchList.forEach(function(item){
var newVal = item.get();
var oldVal = item.last;
if( newVal !== oldVal ){
item.callback(newVal, oldVal);
item.last = newVal;
dirty = true;
}
else{
dirty = false;
}
})
} while(dirty);
refreshView();
}
参考资料:
- https://regularjs.github.io/guide/zh/advanced/dirty.html 脏检查: 数据绑定的秘密
- https://segmentfault.com/a/1190000006599500 剖析Vue原理&实现双向绑定MVVM
- https://github.com/xufei/blog/issues/10 徐飞 Angular沉思录(一)数据绑定
- http://www.lucaongaro.eu/blog/2012/12/02/easy-two-way-data-binding-in-javascript/ Easy Two-Way Data Binding in JavaScript
- http://www.cnblogs.com/jingwhale/p/5117419.html Angular数据双向绑定
- http://www.cnblogs.com/wilber2013/p/5811810.html JavaScript实现简单的双向绑定
- http://www.que01.top/2016/05/03/two-way-bind/ MVVM基础之双向绑定原理
- http://www.cnblogs.com/TomXu/archive/2012/03/02/2355128.html 深入理解JavaScript系列(32):设计模式之观察者模式
- http://ks.netease.com/blog?id=6679 Vue框架核心之数据劫持
- http://ks.netease.com/blog?id=528 AngularJS 数据双向绑定揭秘