动态数据绑定之监听对象变化

动态数据绑定是MVVM框架中最基础的的一个功能,简单描述就是:将数据和视图进行绑定,当数据发生改变时,视图随之改变,更深层次一点,数据绑定包括单向数据绑定和双向数据绑定。

本文从数据绑定中的问题出发,一步一步的来实现这个功能。

问题一

给定任意一个对象,如何监听其属性的读取与变化?也就是说,如何知道程序访问了对象的哪个属性,又改变了哪个属性?

举个例子:

1
2
3
4
5
6
7
8
let app = new Observer({
name: 'DogJun',
address: 'shenzhen'
})
//要实现的结果如下
app.data.name //你访问了name
app.data.address = 'Beijing' //你设置了address, 新的值为 Beijing

实现这样的一个Observer并不难,在此我们暂且不考虑数组的情况,只针对传入的参数为对象。如果对ES6和ES5都熟悉的话,可以立刻想到针对上述场景,可以有两种的实现方式:

  • 采用ES6中的proxy,对目标对象的属性进行拦截处理
  • 采用ES5中的defineProperty,为目标对象的属性添加setter和getter

ES6中的proxy方法详细介绍可看阮一峰老师的《ECMAScript 6入门》地址:http://es6.ruanyifeng.com/#docs/proxy

第一种实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Observer(data){
return new Proxy(data, {
get: function(target, key){
if(key in target){
console.log('你访问了' + key);
return target[key];
}else{
throw new Error('key does not exist')
}
},
set: function(target, key, newVal){
console.log('你设置了' + key);
console.log('新的' + key + '=' + newVal);
target[key] = newVal;
}
})
}
let app = new Observer({
name: 'DogJun',
address: 'shenzhen'
})

第二种实现方式:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 观察者构造函数
function Observer(data) {
this.data = data;
this.walk(data);
}
let p = Observer.prototype;
// 此函数用于遍历对象的各个属性
// 采用递归的思路
// 因为我们要为对象的每个属性绑定getter和setter
p.walk = function(obj) {
let val;
for(let key in obj) {
// 这里要用hasOwnProperty进行过滤
// 因为for...in 循环会把对象原型链上的所有可枚举的属性都循环出来
// 而我们想要的仅仅是这个对象本身拥有的属性
if(obj.hasOwnProperty(key)) {
val = obj[key];
this.convert(key,val);
}
}
};
p.convert = function(key,val) {
Object.defineProperty(this.data,key,{
enumerable: true,
configurable: true,
get: function() {
console.log('你访问了' + key);
return val;
},
set: function(newVal) {
console.log('你设置了' + key);
console.log('新的' + key + ' = ' + newVal);
if(newVal === val) return;
// getter或setter和writable不能同时设置,在没有设置writable时默认为false,故要改变对象的值,必须加上 val = newVal
val = newVal;
}
})
}
let data = {
name: "dogjun",
city: "shenzhen"
};
let app = new Observer(data);

问题二

如果传入的参数对象是一个“比较深”的对象(也就是其属性值也可能是对象),那该怎么办?

举个例子:

1
2
3
4
5
6
7
8
9
10
let app = new Observer({
basicInfo: {
name: 'DogJun',
age: 25
},
address: 'shenzhen'
})
//要实现的结果如下
app.data.basicInfo.name //你访问了basicInfo,你访问了name

基于问题一的第二种实现方式做改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 此函数用于深层次遍历对象的各个属性
// 采用递归的思路
// 因为我们要为对象的每个属性绑定getter和setter
p.walk = function(obj) {
let val;
for(let key in obj) {
// 这里要用hasOwnProperty进行过滤
// 因为for...in 循环会把对象原型链上的所有可枚举的属性都循环出来
// 而我们想要的仅仅是这个对象本身拥有的属性
if(obj.hasOwnProperty(key)) {
val = obj[key];
// 这里进行判断,如果还没遍历到最底层,继续new Observer
if(typeof val === 'object') {
new Observer(val);
}
this.convert(key,val);
}
}
};

问题三

如果设置新的值是一个对象的话,新设置的对象的属性是否能继续响应getter和setter呢?

举个例子:

1
2
3
4
5
6
7
8
9
10
11
let app = new Observer({
basicInfo: {
name: 'DogJun',
age: 25
},
address: 'shenzhen'
})
//要实现的结果如下
app.data.basicInfo = {like: 'NBA'}//你设置了basicInfo,新的basicInfo为{like: 'NBA'}
app.data.basicInfo.like //你访问了basicInfo,你访问了like

基于问题一的第二种实现方式,在setter中做相应的改进:

1
2
3
4
5
6
7
8
9
10
11
set: function(newVal) {
self.watch[key](newVal);
console.log('你设置了' + key);
console.log('新的' + key + ' = ' + newVal);
if(typeof newVal =="object"){
new Observer(newVal);
}
if(newVal === val) return;
// getter或setter和writable不能同时设置,在没有设置writable时默认为false,故要改变对象的值,必须加上 val = newVal
val = newVal;
}

问题四

考虑传递回调函数。在实际应用中,当特定数据发生改变的时候,我们是希望做一些特定的事情,而不是每一次只能打印出来一些信息,所以,我们需要支持传入回调函数的功能。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
let app = new Observer({
name: 'DogJun',
age: 25,
address: 'shenzhen'
})
app.$watch('age', function(age){
console.log(`我的年龄变了,现在是:${age}岁了`);
})
app.data.basicInfo.age = 20;//输出:'我的年龄变了,现在已经是20岁了'

针对上述场景,我们需要实现$watch这个API,每当年龄发生改变的时候触发相应的回调函数。这个API的实现可以很有多种方式,在此我们采用事件的方式来实现,通俗的讲就是实现一个通用的事件模型,每次$watch一个属性相当于注册了一个监听事件,当属性发生改变的则触发对应的事件,这样做的优势是可以为同一个属性通过事件模型来注册多个回调函数。

下边是一个不完整的简易事件模型:

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
//实现一个事件
function Event(){
this.events = {};
}
Event.prototype.on = function(attr, callback){
if(this.events[attr]){
this.events[attr].push(callback);
}else{
this.events[attr] = [callback];
}
}
Event.prototype.off = function(attr){
for(let key in this.events){
if(this.events.hasOwnProperty(key) && key === attr){
delete this.events[key];
}
}
}
Event.prototype.emit = function(attr, ...arg){
this.events[attr] && this.events[attr].forEach(function(item){
item(...arg);
})
}

有了上述事件模型后,每次new一个Observer的实例时,就new一个Event实例出来用来管理Observer实例中的所有事件;然后通过$watch API来为Observer实例注册属性的监听事件,每次当属性改变的触发相应的事件队列。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 观察者构造函数
function Observer(data) {
this.data = data;
this.walk(data);
this.eventsBus = new Event();
}
let p = Observer.prototype;
// 此函数用于深层次遍历对象的各个属性
// 采用递归的思路
// 因为我们要为对象的每个属性绑定getter和setter
p.walk = function(obj) {
let val;
for(let key in obj) {
// 这里要用hasOwnProperty进行过滤
// 因为for...in 循环会把对象原型链上的所有可枚举的属性都循环出来
// 而我们想要的仅仅是这个对象本身拥有的属性
if(obj.hasOwnProperty(key)) {
val = obj[key];
// 这里进行判断,如果还没遍历到最底层,继续new Observer
if(typeof val === 'object') {
new Observer(val);
}
this.convert(key,val);
}
}
};
p.convert = function(key,val) {
let self = this;
Object.defineProperty(this.data,key,{
enumerable: true,
configurable: true,
get: function() {
console.log('你访问了' + key);
return val;
},
set: function(newVal) {
console.log('你设置了' + key);
console.log('新的' + key + ' = ' + newVal);
// 触发$watch函数
self.eventsBus.emit(key,val,newVal);
if(typeof newVal =="object"){
new Observer(newVal);
}
if(newVal === val) return;
// getter或setter和writable不能同时设置,在没有设置writable时默认为false,故要改变对象的值,必须加上 val = newVal
val = newVal;
}
})
}
p.$watch = function(attr,callback) {
this.eventsBus.on(attr,callback);
}
let data = {
user: {
name: "dogjun",
},
address: {
city: "shenzhen"
},
age: '25'
};
let app = new Observer(data);
app.$watch('age', function(oldVal, newVal){
console.log(`我的年龄变了,原来是: ${oldVal}岁,现在是:${newVal}岁了`);
});
app.data.age = '45';

总结一下本文仍然需要解决的问题:

  • 当传入的参数为数组时,如何监听数组对象的变化
  • 深层对象属性的事件回调监听,或者描述为:对象的深层属性值发生变化后如何向上传递到顶层
  • 动态数据与视图的绑定,如何绑定,当数据变化后如何触发视图的自动刷新
  • 问题四中只可以监听对象的第一层属性,对于深层次的属性并不能有效监听