数据双向绑定以及Object.defineProperty函数

关于双向数据绑定

当我们在前端开发中采用MV*的模式时,M-model,指的是模型,也就是数据,V-view,指的是视图,也就是页面展现的部分。通常,我们需要编写代码,将从服务器获取的数据进行“渲染”,展现到视图上。每当数据有变更时,我们会再次进行渲染,从而更新视图,使得视图与数据保持一致。也就是:

而另一方面,页面也会通过用户的交互,产生状态、数据的变化,这个时候,我们则编写代码,将视图对数据的更新同步到数据,以致于同步到后台服务器。也就是:

不同的前端 MV* 框架对于这种 Model 和 View 间的数据同步有不同的处理。在 Backbone 中,Model 到 View 的数据传递,可以在 View 中监听 Model 的 change 事件,每当 Model 更新,View 中重新执行 render。而 View 到 Model 的数据传递,可以监听 View 对应的 DOM 元素的各种事件,在检测到 View 状态变更后,将变更的数据发送到 Model。相较于 Backbone,AngularJS 所代表的 MVVM 框架则更进一步,从框架层面支持这种数据同步机制,而且是双向数据绑定:

不过在不同的 MVVM 框架中,实现双向数据绑定的技术有所不同。

AngularJS 采用“脏值检测”的方式,数据发生变更后,对于所有的数据和视图的绑定关系进行一次检测,识别是否有数据发生了改变,有变化进行处理,可能进一步引发其他数据的改变,所以这个过程可能会循环几次,一直到不再有数据变化发生后,将变更的数据发送到视图,更新页面展现。如果是手动对 ViewModel 的数据进行变更,为确保变更同步到视图,需要手动触发一次“脏值检测”。

VueJS 则使用 ES5 提供的 Object.defineProperty() 方法,监控对数据的操作,从而可以自动触发数据同步。并且,由于是在不同的数据上触发同步,可以精确的将变更发送给绑定的视图,而不是对所有的数据都执行一次检测。

关于Object.defineProperty

这个函数接受三个参数,一个参数是obj,表示要定义属性的对象,一个参数是prop,是要定义或者更改的属性名字,另外是descriptor,描述符,来定义属性的具体描述。
Object.defineProperty(obj, prop, descriptor)

下面的是实例代码,obj是一个没有属性的空对象,然后”key”是属性名,{}大括号里面定义了要给属性赋值的情况,value代表属性的值,proto代表继承属性的性质,这里面还有其他的选项。比如configurable,enumerable,writable等默认是false的。

1
2
3
4
5
6
7
8
9
// using __proto__
var obj = {};
Object.defineProperty(obj, 'key', {
__proto__: null, // no inherited properties
value: 'static' // not enumerable
// not configurable
// not writable
// as defaults
});

我们通过控制台的结果来感受一下writable为false的作用。我们发现,就算对”key”属性重新赋值了,它的属性仍然保持不变。

控制台结果

descriptors(描述符)分成两种,一种是data descriptors,另外一种是 accessor descriptors.两种的descriptors有两个必选项,configurable和enumerable

configurable
true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.Defaults to false
.

代表这个属性的descriptor也就是描述是可以更改的,这个熟悉也能从对象上面删除,默认false,也就是不能更改跟属性有关的任意值,如果我重新对这个属性进行定义的话,会提示出错,同时也不能删除。

configurable

enumerable
true if and only if this property shows up during enumeration of the properties on the corresponding object.Defaults to false
.

代表这个属性能够通过for in或者Object.keys
来遍历。默认为false

关于enumerable的属性

A data descriptor有两个可选项.

value
The value associated with the property. Can be any valid JavaScript value (number, object, function, etc).Defaults to undefined
.

这个选项为属性赋值,可以是任意的JavaScript值,默认为undefined

writable
true if and only if the value associated with the property may be changed with an assignment operator.Defaults to false
.

writable表示能不能够重写属性值,默认为false

accessor descriptor也有两个关键的属性。

get
A function which serves as a getter for the property, or undefined
if there is no getter. The function return will be used as the value of property.Defaults to undefined
.
set

定义了一个函数,作为属性的getter,如果没有getter就为undefined 默认为undefined

set
A function which serves as a setter for the property, or undefined
if there is no setter. The function will receive as only argument the new value being assigned to the property.Defaults to undefined
.

同get

这里面有一点是,可能会从原型链上面继承相应的属性,如果想避免这种情况,可以写get。所以可以用proto: null

下面是一个可爱的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var o = {}; // Creates a new object 创造对象
// Example of an object property added with defineProperty with a data property descriptor
Object.defineProperty(o, 'a', {
value: 37,
writable: true,
enumerable: true,
configurable: true
});
// 'a' property exists in the o object and its value is 37
// Example of an object property added with defineProperty with an accessor property descriptor
var bValue = 38;
Object.defineProperty(o, 'b', {
get: function() { return bValue; },
set: function(newValue) { bValue = newValue; },
enumerable: true,
configurable: true
});
o.b; // 38
// 'b' property exists in the o object and its value is 38
// The value of o.b is now always identical to bValue, unless o.b is redefined
// You cannot try to mix both:
Object.defineProperty(o, 'conflict', {
value: 0x9f91102,
get: function() { return 0xdeadbeef; }
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors

执行结果

第一段代表定义了一个data descriptor,第二段代表定义了accessor descriptor,通get定义了取值操作,第三段代码告诉我们这两种不能混用。

视图和数据变化绑定

而vue.js主要利用了accessor descriptors的set和get来更新视图,这里看到的这个例子挺好,是一个简单的绑定。
对于一个html页面

1
2
3
4
<div>
<p>你好,<span id='nickName'></span></p>
<div id="introduce"></div>
</div>

设置一个数据的属性的getter和setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//视图控制器
var userInfo = {};
Object.defineProperty(userInfo, "nickName", {
get: function(){
return document.getElementById('nickName').innerHTML;
},
set: function(nick){
document.getElementById('nickName').innerHTML = nick;
}
});
Object.defineProperty(userInfo, "introduce", {
get: function(){
return document.getElementById('introduce').innerHTML;
},
set: function(introduce){
document.getElementById('introduce').innerHTML = introduce;
}
})

然后就能愉快地绑定数据交互了。

1
2
userInfo.nickName = "xxx";
userInfo.introduce = "我是xxx,我来自云南,..."

vue.js的数据变动

但是,这个例子只是数据和dom节点的绑定,而vue.js更为复杂一点,它在网页dom和accessor之间会有两层,一层是Wacher,一层是Directive,比如以下代码。

1
2
3
4
var a = { b: 1 }
var vm = new Vue({
data: data
})

把一个普通对象(a={b:1})传给 Vue 实例作为它的 data 选项,Vue.js 将遍历它的属性,用Object.defineProperty 将它们转为 getter/setter,如图绿色的部分所示。
每次用户更改data里的数据的时候,比如a.b =1,setter就会重新通知Watcher进行变动,Watcher再通知Directive对dom节点进行更改。