@GaoyuanHfut
2018-05-28T10:13:20.000000Z
字数 4017
阅读 406
vue.js
对于数据双向绑定,我们需要考虑的问题如下:
而Model的变化监听方式可以有多种,主要有以下几种: 发布订阅模式(Backbone),数据劫持(VueJS,AvalonJS),数据脏检查(Angularjs,RegularJS)
发布订阅模式也称为观察者模式。直观地说,就是有一家报社和很多用户,报社就是发布者,用户就是订阅者。每当报社有新的报纸时,由于订阅者订阅了报纸,他们能第一时间收到新的报纸。当然订阅者也可以取消订阅。

我的理解,原生JS中的事件就是一种观察者模式。比如鼠标的点击事件,只要它点击了,以addEventListener方式订阅它的回调函数就会第一时间收到通知。除了现成的事件,JS的创建自定义事件看起来更直观。
var event = new Event('build');
// 订阅者订阅事件.
elem.addEventListener('build', function (e) { ... }, false);
// 发布事件.
elem.dispatchEvent(event);
在数据发生变化的时候,我们发布一个叫‘model-update’的事件。类似,当视图发生变化的时候,我们发布一个叫‘ui-update’的事件。那么,在这些事件发生时想要做什么动作只要让它去订阅这些事件即可。
// 更新数据
function updateData(attr, value){
data[attr] = value;
pubSub.publish('model-update', attr, value);
}
// 订阅ui-update事件
pubSub.subscribe('ui-update', function(attr, value){
updateData(attr, value);
});
// 订阅model -update事件
pubSub.subscribe('model-update', function(attr, value){
//更新视图中所有单向绑定的值,用类似ng-bind的形式
for(var attr in data){
if( bindingsMap[attr] ){
bindingsMap[attr].forEach(function(item, index){
item.innerHTML = data[attr];
})
}
//更新视图中所有双向绑定的值,用类似ng-model的形式
if(modelsMap[attr]){
modelsMap[attr].forEach(function(item, index){
item.value = data[attr];
})
}
}
});
//视图数据修改,发布ui-update事件
document.addEventListener('keyup', function( e ){
var ele = e.target;
var attr = ele.getAttribute('yc-model');
pubSub.publish('ui-update', attr, ele.value);
})
每次更新数据用updateData函数,这个函数执行了赋值操作之后会发布‘model-update’事件,这样就手动地解决了数据到视图这方向的更新问题了。
html部分:
<input id="ipt" type="text" name="">
<p id="a"></p>
js部分:
var $scope = {
data:''
}
a.innerHTML = ''
setInterval(function(){
a.innerHTML = $scope.data
},60)
ipt.oninput = function(){
$scope.data = ipt.value
}
AngularJS的数据双向绑定是基于数据的脏检查机制的,考虑到性能问题,他当然不是定时去检查的。大体意思上来说,就是记录所有变量的当前值,当发生某些操作之后,通过digest进入脏检查环节。对比最近的一次值和现在的值是否一致,不一致则实现页面的更新,然后再执行一次直到数据不再发生变化。
触发脏检查时全部遍历一次watch队列,实现视图的更新。
那么,什么时候会触发脏检查呢?据说有以下几种情况。
DOM事件,譬如用户输入文本,点击按钮(ng-click)等
XHR响应事件 (location)
Timer事件(interval)
执行apply()
最后一种情况应该是统一的入口,只不过前几种情况会自动调用这个入口而已。其他情况下,用户需要手动进入脏检查的话,就要执行了。
最后是我们的主角,vue双向绑定的基本方法,数据劫持.
在此之前介绍如何用Object.defineProperty()实现双向数据绑定。
我们知道一个存储器属性有四个属性描述符:get,set,configurable,enumerable。
如何创建一个存储器属性:
var user = {
name: ''
}
Object.defineProperty(user, 'nickname', {
configurable: true,
enumerable: true,
get: function() {
return this.name
},
set: function(value) {
this.name = value
}
})
当存在getter,setter函数时,属性的赋值操作会触发setter函数的执行,获取操作会触发getter函数的执行。按行业上的术语来说,这样的方法称之为数据劫持。举个栗子。
function defineProperty(obj, attr, value){
var _value;
Object.defineProperty(obj, attr, {
get:function (){
console.log('get');
return _value;
},
set:function (val){
_value = val;
console.log('监听到数据发生了变化 ');
}
})
obj[attr] = value;
}
var data = {};
defineProperty(data, 'name', "Claire_Yecao"); // "监听到数据发生了变化"
data.name; // get Claire_Yecao
var obj = {name:'pp'}
console.log(obj.name)//pp
Object.defineProperty(obj,'name',{
get:function(){
return 1
},
set:function(newVal){
console.log(newVal)
}
})
console.log(obj.name)//??
obj.name = 2;
console.log(obj.name)//??
有了以上的方法之后,我们不难知道,当数据(对象)发生变化,只要在setter函数中发布就好。结合发布订阅者模式,将手动更新数据的updateData函数变成赋值操作,对象会自动执行setter函数,然后就能发布‘model-update’事件了。
想想还有复杂的问题,要是数据的类型是数组,而用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 '检测数据发生变化'
回到vue本身上来, 首先Vue会使用documentfragment劫持根元素里包含的所有节点,这些节点不仅包括标签元素,还包括文本,甚至换行的回车。
然后Vue会把data中所有的数据,用defindProperty()变成Vue的访问器属性,这样每次修改这些数据的时候,就会触发相应属性的get,set方法。
接下来编译处理劫持到的dom节点,遍历所有节点,根据nodeType来判断节点类型,根据节点本身的属性(是否有v-model等属性)或者文本节点的内容(是否符合{{文本插值}}的格式)来判断节点是否需要编译。对v-model,绑定事件当输入的时候,改变Vue中的数据。对文本节点,将他作为一个观察者watcher放入观察者列表,当Vue数据改变的时候,会有一个主题对象,对列表中的观察者们发布改变的消息,观察者们再更新自己,改变节点中的显示,从而达到双向绑定的目的。