🔒 实现一个MVVM框架(仿Vue.js)

通过手写一个MVVM框架,从而理解Vue.js的响应式原理
涉及到的知识点包括:
Object.defineProperty 实现数据拦截
观察者模式

Object.defineProperty 实现数据拦截

语法:Object.defineProperty(obj, key, descriptor)

基本用法

1
2
3
4
5
var obj = {a:1,b:2}
Object.defineProperty(obj, 'c', {
value : 3
})
console.log(obj) // {a: 1, b: 2, c: 3}

configurable 可否删除

1
2
3
4
5
6
7
var obj = {a:1}
Object.defineProperty(obj, 'b', {
configurable: false,
value : 2
})
delete obj.b //无法删除

configurable 的值设置为 false 后(如果没设置,默认就是 false),就无法删除该属性。

enumerable 可否遍历

1
2
3
4
5
6
7
8
var obj = {a: 1};
Object.defineProperty(obj, 'b', {
value: 2,
enumerable: false
});
for(var key in obj){
console.log(key) // 只输出 a, 不输出b
}

设置 enumerable 属性为 false 后,遍历对象的时候会忽略当前属性(如果未设置,默认就是 false不可遍历)。

writable 可否修改

1
2
3
4
5
6
var obj = {a:1,b:2}
Object.defineProperty(obj, 'c', {
value : 3,
writable: false
})
obj.c = 111 //修改失败

get和set

注意,getvalue不能同时存在!

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {};
Object.defineProperty(obj, 'age', {
get(){
console.log('正在获取age...');
return age;
},
set(newVal){
console.log('正在设置age...');
age = newVal;
}
});
obj.age = 100;
console.log(obj.age); // 100

  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数newVal,并将该参数的新值分配给该属性。默认为 undefined。

数据劫持

写一个数据劫持函数observe(),效果如下:

1
2
3
4
5
6
7
8
9
var data = {
a: 1,
b: {c:2}
};
observe(data);
console.log(data.b.c)
data.b = [111,222,333]
data.b

说干就干!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function observe(data){
if(!data || typeof data !== 'object'){//如果data不存在,或者它不是个对象
return;
} else{
for(let key in data){
let val = data[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get(){
return val;
},
set(newVal){
console.log(`${val} => ${newVal}`);
val = newVal;
}
});
if(typeof val === 'object'){//如果value是个对象,则递归
observe(val);
}
}
}
}

以上,observe方法实现了数据的劫持

观察者模式(发布/订阅模式)

一个典型的观察者模式应用场景是用户在一个网站订阅主题

  1. 多个用户(观察者,Observer)都可以订阅某个主题(Subject)
  2. 当主题内容更新时订阅该主题的用户都能收到通知

以下是代码实现

Subject是构造函数,new Subject() 创建一个主题对象,该对象内部维护订阅当前主题的观察者数组。主题对象上有一些方法,如添加观察者(addObserver)、删除观察者(removeObserver)、通知观察者更新(notify)。 当notify 时实际上调用全部观察者 observer 自身的 update 方法。

Observer 是构造函数,new Observer() 创建一个观察者对象,该对象有一个 update 方法。

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
class Subject {
constructor() {
this.observers = []
};
addObserver(observer) {
this.observers.push(observer)
};
removeObserver(observer) {
let index = this.observers.indexOf(observer);
if(index > -1){
this.observers.splice(index, 1);
}
};
notify() {
this.observers.forEach(observer=> {
observer.update();
});
};
}
class Observer{
constructor(name) {
this.update = function() {
console.log(name + '更新...')
}
}
};
let observer1 = new Observer('observer1')
let observer2 = new Observer('observer2')
let subject = new Subject()
subject.addObserver(observer1)
subject.addObserver(observer2)
subject.notify()

上面的代码中,主题被观察者订阅的写法是 subject.addObserver(observer), 不是很直观,改写成 observer.subscribeTo(subject)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Observer{
constructor() {
this.update = function() {
console.log('更新...');
}
};
subscribeTo(subject) {
subject.addObserver(this);
}
};
let subject = new Subject() //发布主题
let observer = new Observer()
observer.subscribeTo(subject) //观察者订阅主题
subject.notify() //通知更新

MVVM单向绑定的实现

MVVM (Model-View-ViewModel) 是一种用于把数据和视图层分离的设计模式。
MVVM 中的 Model 表示应用程序使用的数据,比如一个用户账户信息(名字、头像、电子邮件等)。Model 保存信息,但通常不处理行为,不会对信息进行再次加工。数据的格式化是由View 处理的。行为一般认为是业务逻辑,封装再 ViewModel 中。
View 是与用户进行交互的桥梁。
ViewModel 充当数据转换器

  • 将 Model 信息转换为 View 的信息
  • 将命令从 View 传递到 Model。

假设有如下代码,data 里的name会和视图中的绑定,修改 data 里的值,会直接引起视图中对应数据的变化。

1
<div>{{ name }}</div>
1
2
3
4
5
new Vue({
data:{
name:'xxx'
}
})

实现一个Vue

1
2
3
4
5
6
7
8
class Vue(){
constructor(opts){
this.init(opts);
observer(this.$data);
this.compile();
};
}

-------------本文结束感谢您的阅读-------------