### 几种实现双向绑定的做法
主流的mvc(vm)框架,都实现了单向数据绑定,而双向数据绑定无非就是在单向绑定的基础上给可输入元素(input,textarea)等添加了change(input)事件,来动态修改model和view,并没有多高深。
实现数据绑定的做法
1. 发布者-订阅者(backbone.js)
2. 脏值检查(angular.js)
3. 数据劫持(vue.js)
#### 发布者-订阅者
一般通过sub,pub的方式实现数据和视图的绑定监听,更新数据的方式通常做法 vm.set('property',value),这种方式毕竟太low,我们更希望**vm.property=value这种方式更新数据,同时自动更新视图**。
以下两种方式:
#### 脏值检查
angular.js是通过脏值检测的方式对比数据是否有变更,来决定是否更新视图,最简单的方式就是
**通过setInterval()定时轮询检测数据变动**,当然google不会这么low,**angular只有在指定的事件触发时进入脏值检测**,大致如下:
* DOM事件,譬如用户输入文本,点击按钮等(ng-click)
* XHR响应事件($http)
* 浏览器Location变更事件($location)
* Timer事件($timeout,$interval)
* 执行$digest()或$apply()
### 数据劫持
vue.js采用**数据劫持结合发布者-订阅者模式**的方式,通过**Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调**。
### 整理思路
vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的。
要实现mvvm的双向绑定
1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2. 实现一个指令解析器compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定
### 1. 实现Observer
可以利用Object.defineProperty()来监听属性变动
那么将**需要observe的数据对象进行递归遍历,包括自属性对象的属性,都加上setter和getter**
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能坚挺到了数据变化。
```
var data={name:'hhh'}
observe(data);
function observe(data){
if(!data || typeof data !=='object'){
return;
}
//去除所有属性遍历
Object.keys(data).forEach(function(key){
defineReactive(data,key,data[key]);
});
};
function defineReactive(data,key,val){
observe(val);//监听子属性
Object.defineProperty(data,key,{
enumerable:true,//可枚举
configurable:false,//不能在define
get:function(){
return val;
},
set:function(newVal){
console.log('监听到值的变化了',val,newVal);
val=newVal;
}
})
}
```
这样我们已经可以监听每个数据的变化了,那么监听到变化怎么通知订阅者呢?接下来需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,在调用订阅者的update方法
```
function defineReactive(data, key, val){
var dep = new Dep();
observe(val);
Object.defineProperty(data,key,{
//省略
set:function(newVal){
if(val === newVal) return;
console.log('监听到值的变化了',val,newVal);
val=newVal;
dep.notify();//通知所有订阅者
}
})
}
function Dep(){
this.subs=[];
}
Dep.prototype={
addSub: function(sub){
this.subs.push(sub);
},
notify:function(){
this.subs.forEach(function(sub){
sub.update();
})
}
}
```
那么谁是订阅者?怎么网订阅器里添加订阅者?
上面的思路整理中,我们已经明确订阅者应该是Watcher,而且 var dep = new Dep();是在defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们在getter里面动手脚
```
// Observer.js
var dep = new Dep();
observe(val); // 监听子属性
Object.defineProperty(data, key, {
get: function(){
//由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher,添加完移除
Dep.target && addDep(Dep.target);
return val;
}
})
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
//Watcher.js
Watcher.prototype = {
get:function(key){
Dep.target = this;
this.value = data[key];//这里会触发属性的getter,从而添加订阅者
Dep.target = null
}
}
```
这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能。
### 2. 实现Compile
compile:解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
因为遍历接卸的过程有多次操作dom,为了提高效率,将根节点el转换成碎片fragment进行接卸编译操作,解析完成,在将fragment添加回原来的真是dom节点中
#### Object.defineProperty()方法
会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象
`Object.defineProperty(obj, prop, descriptor)`
descriptor:将被定义或修改的属性描述符
返回值:传递给函数的对象。
描述:该方法允许精确添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的(for in,Object.keys),这些属性的值可以被改变,也可以被删除,这个**方法允许修改默认的额外选项(配置)**。默认情况下,使用Object.defineProperty添加的属性是不可修改的。
#### 属性描述符
**数据描述符和存取描述符**
数据描述符:是一个具有值的属性,该值可能是可写的,也可能不是可写的。
存取描述符:有getter-setter函数对描述的属性。
描述符必须是这两种形式之一。
数据描述符和存取描述符均具有以下可选键值:
configurable:true,**true时,该属性描述符才能够被改变**,默认false。
enumerable:**true时,该属性才能够出现对象的枚举属性中**。默认false。
**数据描述符**同时具有以下可选键值:
value:属性值,默认undefined
writable:true时,value才能被赋值运算符改变,默认false。
**存取描述符**同时具有以下可选键值:
get:一个给属性提供getter的方法**,如果没有getter则为undefined**,当访问该属性时,改方法会被执行,方法执行时没有参数传入,但是会传入this对象(this并以一定是定义改属性的对象)。
set:一个给属性提供setter的方法,如果没有setter则为undefined,当属性值修改时,触发改方法。
描述符可同时具有的键值
如果一个描述符**不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符**。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生**一个异常**。
记住:这些选项不一定是自身属性,如果是继承来的也要考虑。
为了确认保留这些默认值,你可能要在这之前**冻结 Object.prototype**,明确指定所有的选项,或者通过 **Object.create(null)将__proto__属性指向null**。