模拟Vue的MVVM

目的

为了真正的了解MVVM的核心思想,本文以Vue框架为切入点,手写模拟实现Vue框架的双向数据绑定,本文适合想进一步了解Vue底层的同学,当然对于想彻底搞懂MVVM原理的同学也会有所启发。

MVVM概念

image

MVVM的概念可以参考很多文章,比如这篇,其最核心的就是实现视图视图模型的双向绑定。本文主要探究MVVM在Vue中的实现,下面将会围绕这个主题进行展开。

Object.defineProperty()

在讲解Vue的双向绑定之前,先来回顾一下Object.defineProperty(),在MDN中也有详细的解释,已经知道的同学可以跳过这个章,没看过的同学建议往下看。

1
2
3
var person = {};    
person.name = "xuan";
console.log(person);

上述代码在我们的日常开发中司空见惯,打印结果如下:

image

现在我们改写一下

1
2
3
4
5
6
7
8
var person = {};
Object.defineProperty(person,
"name",
{
value:"xuan"
}
);
console.log(person);

在IE8以上浏览器中打印结果如下:

image

一点问题没有。下面我们改一下这个对象属性,

1
2
3
4
5
6
7
8
9
var person = {};
Object.defineProperty(person,
"name",
{
value:"xuan"
});
console.log(person);
person.name = "xue";
console.log(person);

结果如下:

image

我们发现结果没有变化,直接使用”对象.属性”的方法已经失效,查一下API,在代码中添加writable属性

1
2
3
4
5
6
7
8
9
10
var person = {};
Object.defineProperty(person,
"name",
{
value:"xuan",
writable:true
});
console.log(person);
person.name = "xue";
console.log(person);

结果如下:

image

现在已经可以修改对象属性了,所以writable属性就是可复写的。在API中还存在configurableenumerable属性,MDN这么说的:

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。

enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

如果觉得不明白,看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var person = {}; 
Object.defineProperty(person,
"name",
{
value:"xuan",
writable:true,
configurable:true,
enumerable:true
});
console.log(person);
delete person.name;
console.log(person);
person.name = "xuan";
person.age = 18;
for(var key in person){
console.log(key);
}

结果如下:

image

简而言之:configurable 就是可删除的,enumerable就是可遍历的。在API中还有两个配置项set和get,废话不说,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 var person = {};
Object.defineProperty(person,
"name",
{
value: "xuan",
writable: true,
configurable: true,
enumerable: true,
set: function (newVal) {
console.log(`此处设置一个新值:${newVal}`)
this.value = newVal;
},
get: function () {
console.log(`此处获取一个值:${this.value}`)
return this.value;
}
});
console.log(person);

结果报错

image

上述错误的意思是我们要把writable和value配置项去掉。为什么?get和set方法是获取和设置值的函数,有了它们就不需要writable和value配置项了。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {};
Object.defineProperty(person,
"name",
{
configurable: true,
enumerable: true,
set: function (newVal) {
console.log(`此处设置一个新值:${newVal}`)
this.value = newVal;
},
get: function () {
console.log(`此处获取一个值:${this.value}`)
return this.value;
}
});
person.name = "xuan";
console.log(person);
console.log(person.name);

结果如下:

image

在设置和获取这个对象的属性时,都会打印相应的信息。一旦对象的信息有所修改,我们都能第一时间发现它,这就是\数据劫持**。当对象的值改变的时候,如果我们能够自动去更新对应的视图,就能够完成从\对象视图模型(VM)=>视图**的单向数据流绑定。

模拟Vue的数据劫持

在讲解数据劫持之前,先来看下面一段代码

1
2
3
<div id="app">
<p>姓名:{name}</p>
</div>
1
2
3
4
5
6
let vue = new Vue({
el:"#app",
data:{
name:"xuan"
}
});

这是一个最最基本的Vue程序,但是我们没有导入Vue的库,现在我们来看界面的效果:

image

现在我们没有看到任何效果,控制台还输出错误。好,我们现在只是搭建了一个空壳,下面我们一步步来实现和完善它。

劫持Vue中的data属性中的对象

我们先来定义一个Vue类,然后劫持传入配置项的data属性中的对象,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Vue(options={}){
this.$options = options;
let obj = this._data = options.data;
for(let key in obj){
var val = obj[key];
Object.defineProperty(obj,key,{
configurable:true,
enumerable:true,
set:function(newVal){
if(newVal === val) return;
console.log("数据已更新")
val = newVal;
},
get:function(){
console.log("数据已获取")
return val;
}
});
}
}

使用$options用于保存Vue的配置参数对象,_data用于存储真正的数据。我们打印下述信息:

1
2
3
console.log(vue);
console.log(vue._data.name="xue");
console.log(vue._data.name)

看一下效果:

image

我们需要的效果实现了!我们把代码整理一下:

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
let vue = new Vue({
el: "#app",
data: {
name: "xuan"
}
});
console.log(vue);

function Vue(options = {}) {
this.$options = options;
let data = this._data = options.data;
observe(data);
}

function observe(data) {
new Observe(data);
}

function Observe(data) {
var keys = Object.getOwnPropertyNames(data);
keys.forEach(function(key) {
var val = data[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function () {
return val;
},
set: function (newVal) {
console.log('获取数据属性 ', val, ' --> ', newVal);
val = newVal;
}});
}, this);
}

我们把数据劫持的代码逻辑写在Observe类中,我们下面就是围绕这个展开。

呃…现在我们的效果真达到了么?我们重新设置一下vue对象

1
2
3
4
5
6
7
8
9
10
11
let vue = new Vue({
el: "#app",
data: {
name: "xuan",
degree:{
name:"master",
time:"3year"
}
}
});
console.log(vue);

结果显示如下

image-20190220220513732

我们发现name属性还是有set和get方法,但是degree对象还嵌套着对象,所以这个时候我们需要递归遍历整个对象。于是添加修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Observe(data) {
if(typeof data!="object") return;
var keys = Object.getOwnPropertyNames(data);
keys.forEach(function(key) {
var val = data[key];
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function () {
console.log('获取数据属性',val);
return val;
},
set: function (newVal) {
console.log('设置数据属性 ', val, ' --> ', newVal);
val = newVal;
}
});
}, this);
}

主要修改两个地方,在var val = data[key]后添加observe(val),对获取的属性值进行观察,然后在设置递归终止条件, if(typeof data!=”object”) return;最后效果如下:

image-20190220220624044

下面再来写一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
let vue = new Vue({
el: "#app",
data: {
name: "xuan",
degree: {
name: "master",
time: "3year"
}
}
});
vue._data.name = {a:1}
console.log(vue);

结果如下:

image-20190220225340987

我们发现name属性其实已经被劫持到了,但是传进去的对象没有被劫持到,所以在set方法里面也需要添加observe函数。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Observe(data) {
if(data==null || typeof data!="object") return;
Object.keys(data).forEach(function(key){
var val = data[key];
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function () {
console.log('获取数据属性',val);
return val;
},
set: function (newVal) {
console.log('设置数据属性 ', val, ' --> ', newVal);
val = newVal;
observe(val);
}
});
});
}

结果如下:

image-20190220225646064

上述基本完成了对于配置对象中data属性的数据劫持

数据代理

## References

界面之下:还原真实的MV*模式