Object.defineProperty和数据双向绑定

概述

在MVVM中,改变数据就意味着改变视图,这意味着我们不再需要在模型和视图之间加入控制器,这种现代化的设计模式使开发变得更清晰简洁。然而对于数据双向绑定的实现,各大流行框架都有自己的实现,其中VueJS的主要黑魔法就是利用了ES5的一个API,Object.defineProperty。

监听器模式

在介绍Object.defineProperty在数据视图绑定的应用之前,需要先学习一个设计模式,监听器模式(也叫观察者模式,或者订阅-发布模式),这并不是一个什么高大上的东西,实际上给DOM添加事件就是一种监听器模式,它的简单实现如下。

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
const store = (function() {
let _value = '';
const _fns = [];
return {
subscribe(fn) {
if(_fns.indexOf(fn) === -1) {
_fns.push(fn);
}
},
unsubscribe(fn) {
const index = _fns.indexOf(fn);
if(index !== -1) {
_fns.splice(index, 1);
}
},
getValue() {
return _value;
},
setValue(value) {
_value = value;
_fns.forEach(function(fn, idx) {
fn.call(null, value);
});
}
};
})();

可以看到,store是一个IIFE执行的返回结果,其中_value用于存储值,_fns用于存储注册的响应函数,当setValue方法被执行时,会自动触发所有的已经注册的方法,这些方法都将接收到store中存储的值作为参数,这个实现使用起来也非常方便。

1
2
3
4
store.subscribe(v => console.log(v));
store.subscribe(v => console.log(v + 3));
store.setValue(5);
// 输出 5 和 8

这里注册两个了响应函数,分别是输出value和value + 3的结果,测试可以发现运行与预期一致。以上就是一个简单的监听器模式和使用方法,可以看到它确实和DOM事件的添加很像,此外,我们还可以在添加时就保存函数变量(不使用匿名函数,使用函数表达式),这样还可以通过unsubscribe来移除响应函数。

基本实现

现在已经对监听器模式有了一个大致的理解了,那它如何配合Object.defineProperty一起使用呢?

Object.defineProperty用于直接在一个对象上定义属性,其使用方法如下。

1
Object.defineProperty(obj, prop, descriptor);

其中obj表示需要被定义新属性的对象,prop是一个字符串,表示新属性,descriptor用于描述相关行为内容,这里我们只关心set和get,具体可查看参考1,来看如下代码。

1
<div id="app">haha</div>
1
2
3
4
5
6
7
8
9
let data = {};
Object.defineProperty(data, 'user', {
get() {
return app.innerHTML;
},
set(val) {
app.innerHTML = val;
}
});
1
data.user = 'Ouyang';

运行如上代码,会发现当我们执行完data.user = ‘Ouyang’这行语句之后,视图也自动更新了。

为了能让这种方式更灵活,可以将它和监听器模式结合在一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const configProperty = function(obj, prop, fn) {
let _val = undefined;
Object.defineProperty(obj, prop, {
get() {
return _val;
},
set(val) {
if(_val === val) {
return;
}
_val = val;
fn.call(obj);
}
});
};

configProperty接收三个参数,第一个是将要被设置属性的对象,第二个是属性名,第三个注册了当属性改变时的响应函数。

configPropert的使用方法也很简单。

1
2
3
4
5
let obj = {};
configProperty(obj, 'user', function() {
app.innerHTML = this.user;
});
obj.user = 'Ouyang';

对一个对象使用configProperty配置完属性之后,在注册响应的监听器,此时,只要改属性的值被设置(且与之前的值不同),则触发注册的注册的函数。

目前的这种实现不能算是监听器模式,因为注册函数只能在一开始就配置属性时就确定(也就是一开始就要确定configProperty的第三个参数),不过要写成监听器模式只需稍加扩展,将注册监听器的方法独立出来即可(需要用到闭包)。

总结

整体而言,这种设计是的视图和模型的之间的关系更明确了,用学术一点的说法就是F(Model) = View,对于这样的设计而言,只要对模型进行修改,视图自动就应该被更新,而不需要多余的操作。

当然,监听器模式配合Object.defineProperty也只是一种实现方式而已,对于模型和视图的处理,前面文章中提到过的redux也是一种非常好而且很流行的实现。

参考

  1. Object.defineProperty() - MDN web docs