MVVM框架主要包含3个部分:model
、view
和 viewmodel
。
- Model:指的是数据部分,对应到前端就是javascript对象
- View:指的是视图部分,对应前端就是dom
- Viewmodel:就是连接视图与数据的中间件
1.双向数据绑定的实现方式
简单的来说,就是框架的控制器层(这里的控制器层是一个泛指,可以理解为控制view行为和联系model层的中间件)和UI展示层(view层)建立一个双向的数据通道。当这两层中的任何一方发生变化时,另一层将会自动作出相应的变化。
一般来说要实现这种双向数据绑定,在前端我目前了解的有三种形式:
- 基于脏检查 angular,regular(网易开发)
- 观察机制
- 封装属性访问器
2.基于脏检查
目前angular,regular的实现都是基于脏检查。当发生某些特定的事情的时候,框架会调用相关的digest方法。内部逻辑就是遍历所有的watcher
,对监控的属性做对比。如果值发生了变化,则执行相应的handler
。
2.1基于regular详细介绍一下:
watcher对象看起来是这样的:
{
get: function(context){...} //获得表达式当前求值,此函数在解析时,已经生成
set: function(){} // 有些表达式可以生成set函数,用于处理赋 值,这个一般用于双向绑定的场景
once: false // 此监听器是否只生效一次
last: undefined// 上一次表达式的求值结果
fn: function(newvalue, oldvalue){} // 即你传入$watch的第二个参数,当值改变时,会调用此函数
// ...
}
当系统进入脏检查阶段,遍历所有的$watch
绑定的watcher
,然后对比watcher.get()
与watcher.last
,如果不同则运行对应的watcher.fn(newvalue, oldvalue)
。然后再进入下一个watcher的检查。
何时进行脏检查?
在Regular中,digest阶段是由$update
方法触发的。
具体源码在regular/src/helper/watcher.js里面
由于regularjs是基于脏检查,所以当不是由regularjs本身控制的操作(如事件、指令)引起的数据操作,可能需要你手动的去同步data与view的数据. $update方法即帮助将你的data同步到view层.
//手动同步
var component = new Regular();
component.data.name = 'leeluolee'
// you need call $update to Synchronize data and view
component.$update();
//自动进入
<div on-click={blog.title='Hello'}>{blog.title}</div>
总结:但是很显然,脏检查是低效的,它的效率基本上取决于你绑定的观察者数量,在Regular中,你可以通过[@(Expression)
](https://regularjs.github.io/reference?syntax-zh#bind-once)元素来控制你的观察者数量。
3.观察者机制
使用ES7中的 Object.observe 方法对对象(或者其属性)进行监控观察,一旦其发生变化时,将会执行相应的handler。这是目前监控属性数据变更最完美的一种方法,语言(浏览器)原生支持,没有什么比这个更好了。唯一的遗憾就是目前支持广度还不行,有待全面推广。
4.封装属性访问器(存取器get,set)
vue.js和avalon.js实现数据双向绑定的原理就是属性访问器。
它使用了ES5中的定义标准属性的Object.defineProperty 方法。
Object.defineProperty(obj, prop, descriptor)
//obj 待修改的对象
//prop 待修改的属性名称
//descriptor 待修改属性的相关描述,要求传入一个对象。
//@{param} descriptor
1.configurable ,属性是否可配置。可配置的含义包括:是否可以删除属性( delete ),是否可以修改属性的 writable 、 enumerable 、 configurable 属性。
2.enumerable ,属性是否可枚举。可枚举的含义包括:是否可以通过 for...in 遍历到,是否可以通过 Object.keys() 方法获取属性名称。
3.writable ,属性是否可重写。可重写的含义包括:是否可以对属性进行重新赋值。
4.value ,属性的默认值。
5.set ,属性的重写器。一旦属性被重新赋值,此方法被自动调用。
6.get ,属性的读取器。一旦属性被访问读取,此方法被自动调用。
Object.defineProperty
使用示例:
var o = {};
Object.defineProperty(o, 'name', { value: 'erik'});
console.log(Object.getOwnPropertyDescriptor(o, 'name'));
// {
value: "erik",
writable: false,
enumerable:false,
configurable: false
}
Object.defineProperty(o, 'age', {
value: 23,
configurable: true,
writable: true
});
console.log(o.age); // 23
o.age = 18;
console.log(o.age); // 18. 因为age属性是可重写的
console.log(Object.keys(o)); // []. name和age属性都不是可枚举的
Object.defineProperty(o, 'sex', {
value: '女',
writable: false
});
o.sex = '男'; // 这里的赋值其实是不起作用的
console.log(o.sex); // '女';
delete o.sex; // false, 属性删除的动作也是无效的
注意:
- Object.defineProperty() 方法设置属性时,属性不能同时声明访问器属性( set 和 get )和 writable 或者 value 属性。 意思就是,某个属性设置了 writable 或者 value 属性,那么这个属性就不能声明 get 和 set 了,反之亦然。
- 因为 Object.defineProperty() 在声明一个属性时,不允许同一个属性出现两种以上存取访问控制。
4.1基于vue使用封装属性访问器
首先,vuejs在实例化的过程中,会对遍历传给实例化对象选项中的data 选项,遍历其所有属性并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
同时每一个实例对象都有一个watcher实例对象,他会在模板编译的过程中,用getter去访问data的属性,watcher此时就会把用到的data属性记为依赖,这样就建立了视图与数据之间的联系。当之后我们渲染视图的数据依赖发生改变(即数据的setter被调用)的时候,watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。这样就实现了所谓的双向数据绑定。