Vue原理及实现

学了一小段时间的Vue,之前多多少少有了解一些它的原理,但是一直没能有机会梳理一遍。而且之前在面试中也被问到过,才发现自己居然说不出来。因此这样让我下定决心自己按照Vue的原理实现一遍简单的MVVM框架。

本文实现除了参考官方源码Vuejs,还参考开源仓库,在此做出说明。

响应式原理

把一个普通JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。Object.defineProperty 是仅ES5支持,且无法shim的特性,这也就是为什么Vue不支持IE8以及更低版本浏览器的原因

了解更多defineProperty

Vue.js是采用Object.definePropertygettersetter,也叫数据劫持,并结合观察者模式来实现数据绑定的。
image

  • Observer数据监听器,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用Object.defineProperty的getter和setter来实现。
  • Compile指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  • Watcher订阅者, 作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。
  • Dep消息订阅器,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发notify函数,再调用订阅者的update方法。

实现

Observer

首先我们使用ES5的Object.defineProperty来实现属性的监听。对要绑定的data对象的属性依次设置gettersetterdata 子属性也需要监听。

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
var Observer = function Observer(data){
this.data = data;
//遍历属性设置响应
this.walk(data);
}
Observer.prototype = {
//遍历属性设置响应
walk: function(obj){
Object.keys(obj).forEach(key => this.defineReactive(obj, key, obj[key]));
},
//设置属性响应
defineReactive: function(data, key, val){
// 递归监听子属性
observe(val);
// 定义setter和getter
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val;
},
set: function(newVal){
if (val === newVal) return;
console.log(key + ': ' + val + ' --> ' + newVal);
if (typeof newVal == 'object') {
observe(newVal);
}
val = newVal;
}
})
}
}
//监听data
function observe(data){
if (typeof data !== 'object') {
return;
}
return new Observer(data);
}

我们监听如下data对象,一切都很好,但是使用Array的push等方法并不会被监听到。

1
2
3
4
5
6
7
8
9
10
var data = {
name: 'hsj',
msg: 'hello',
todos: ['']
};
observe(data);
data.msg = 'hi'; //msg: hello --> hi
data.todos = ['study', 'work']; //todos: --> study,work
data.todos[0] = 'sleep'; //0: study --> sleep
data.todos.push('sleep'); //3 没有监听到变化

为了实现监听数组,我们自然要改写操作数组的方法,但是又不能改写原生Array原型中的方法。简单的方法是使用Object.create继承Array的原型创建一个新对象arrFakeProto,用这个对象替换需要监听的数组的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arrProto = Array.prototype,
arrFakeProto = Object.create(arrProto),
arrMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
arrMethods.forEach(function (method){
var original = arrProto[method];
arrFakeProto[method] = function(){
//为了显示效果,复制一份原始数据打印
var arr = this.concat(),
//这里我们就能监听到变化了,调用原生的数组方法
res = original.apply(this, arguments);
console.log(arr + ' --> ' + this);
return res;
}
})

看看操作数组的监听效果

1
2
3
4
var arr = ['study', 'work'];
Object.setPrototypeOf(arr, arrFakeProto);
arr.push('sleep'); //study,work --> study,work,sleep 3
Array.isArray(arr) //true

看起来达到预期效果了,但是这里其实还有一个小问题,那就是我们使用push操作数组,监听到变化了,但是新元素并没有被监听到。因此对于添加操作pushunshifitsplice添加的新元素需要重新监听。

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
var arrProto = Array.prototype,
arrFakeProto = Object.create(arrProto),
arrMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
arrMethods.forEach(function (method){
var original = arrProto[method];
arrFakeProto[method] = function(){
//为了显示效果,复制一份原始数据打印
var arr = this.concat(),
args = Array.prototype.slice.call(arguments),
inserted = null;
switch (method) {
case 'push':
inserted = args;
break;
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
//监听新元素
if(inserted) observeArray(inserted);
//这里我们就能监听到变化了,调用原生的数组方法
var res = original.apply(this, arguments);
console.log(arr + ' --> ' + this);
return res;
}
})

综上,我们得到完整的observe代码

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
var arrProto = Array.prototype,
arrFakeProto = Object.create(arrProto),
arrMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
arrMethods.forEach(function (method){
var original = arrProto[method];
arrFakeProto[method] = function(){
//为了显示效果,复制一份原始数据打印
var arr = this.concat(),
args = Array.prototype.slice.call(arguments),
inserted = null;
switch (method) {
case 'push':
inserted = args;
break;
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
//监听新元素
if(inserted) observeArray(inserted);
//这里我们就能监听到变化了,调用原生的数组方法
var res = original.apply(this, arguments);
console.log(arr + ' --> ' + this);
return res;
}
})
var Observer = function Observer(data){
this.data = data;
// 遍历属性设置响应
if (Array.isArray(data)) {
// 替换数组原型
Object.setPrototypeOf(data, arrFakeProto);
observeArray(data);
}
else{
this.walk(data);
}
}
Observer.prototype = {
//遍历属性设置响应
walk: function(obj){
Object.keys(obj).forEach(key => this.defineReactive(obj, key, obj[key]));
},
//设置属性响应
defineReactive: function(data, key, val){
// 递归监听子属性
observe(val);
// 定义setter和getter
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val;
},
set: function(newVal){
if (val === newVal) return;
console.log(key + ': ' + val + ' --> ' + newVal);
if (typeof newVal == 'object') {
observe(newVal);
}
val = newVal;
}
})
},
}

以上代码中,array和object分开监听,实际上只监听object,array只监听数组操作方法,不再监听下标,但是新添加的元素如果是对象依然进行监听

1
2
3
4
5
6
7
8
9
10
var data = {
name: 'hsj',
msg: 'hello',
todos: ['']
};
observe(data);
data.msg = 'hi'; //msg: hello --> hi
data.todos = ['study', 'work']; //todos: --> study,work
data.todos.push({'title': 'sleep','time':'22:00'}); //study,work --> study,work,[object Object] , 3
data.todos[2]['time'] = '23:00'; //time: 22:00 --> 23:00

Dep

至此,简单的监听已经实现了,但是还需要做一件事情,那就是在监听到变化后通知订阅者Watcher,因此还需要实现上文说的消息订阅器Dep。消息订阅器实质上在内部维护一个数组,用来收集订阅者(Watcher),当Observer监听数据变动后(在set中触发),(在set中)触发notify函数,再调用订阅者的update方法。

未完,待续。。。。