实现原理

数据劫持 + 发布订阅模式

ES5中提供的 Object.defineProperty,浏览器兼容支持到 IE8+

Object.defineProperty

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
let obj = {};
let song = '发如雪';
obj.singer = '周杰伦';

console.log('=== Object.defineProperty 前 ===')
console.log(obj)

Object.defineProperty(obj, 'music', {
// value: '七里香',
configurable: true, // 可以配置对象,删除属性
// writable: true, // 可写
enumerable: true, // 可枚举
get() { // get,set设置时,不能设置 writable 和 value属性,两者互斥
return song; // 获取 obj.music 调用 get 方法
},
set(val) {
song = val; // 将修改的值重新赋值给 song
}
})

console.log('=== Object.defineProperty 后 ===')
console.log(obj);

delete obj.music

console.log('=== 删除 music ===')
console.log(obj)

obj.music = '听妈妈的话'

console.log('=== 重新赋值 music ===')
console.log(obj)

for (let key in obj) {
// 默认情况下通过 defineProperty 定义的属性是不能被枚举(遍历)的
// 需要设置 enumerable 为 true 才可以
console.log(key);
}

console.log(`获取 obj.music:${obj.music}`);

obj.music = '夜曲';

console.log(`重新赋值后,获取 obj.music:${obj.music}`);

输出结果

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
=== Object.defineProperty 前 ===
{
singer: "周杰伦",
__proto__: Object
}

=== Object.defineProperty 后 ===
{
music: "发如雪",
singer: "周杰伦",
get music: ƒ get(),
set music: ƒ set(val),
__proto__: Object
}

=== 删除 music ===
{
singer: "周杰伦",
__proto__: Object
}

=== 重新赋值 music ===
singer: "周杰伦"
music: "听妈妈的话"
__proto__: Object

singer
music

获取 obj.music:听妈妈的话

重新赋值后,获取 obj.music:夜曲

数据劫持

为什么要做数据劫持?

  • 观察对象,给对象增加 Object.defineProperty
  • vue 特点是不能新增不存在的属性,不存在的属性没有 get、set
  • 深度响应,每次赋予一个新对象时会给这个新对象增加 defineProperty (数据劫持)
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
function Observe(data){
// 数据劫持就是给对象增加 get、set
for (let key in data){
let val = data[key]
observe(val) // 递归,实现深度数据劫持
Object.defineProperty(data, key, {
configurable: true,
get(){
return val
},
set(newVal){
if(val == newVal){
return
}
val = newVal
observe(newVal)
}
})
}
}

function observe(data){
if(!data || typeof data !== "object") return
return new Observe(data)
}

数据编译

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
function Mvvm(options = {}){
new Compile(options.el, this)
}

function Compile(el, vm){
vm.$el = document.querySelector(el)
let fragment = document.createDocumenyFragment()

while(child = vm.$el.firstChild){
fragment.appendChild(child)
}

function replace(frag){
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent
let reg = /\{\{(.*?)\}\}/g

if(node.nodeType === 3 && reg.test(txt)){
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placeholder) => {
console.log(placeholder);
// 匹配到的分组 如:song, album.name, singer...
new Watcher(vm, placeholder, replaceTxt);
// 监听变化,进行匹配替换内容

return placeholder.split('.').reduce((val, key) => {
return val[key];
}, vm);
})
}
replaceTxt()
}

if(node.childNodes && node.childNodes.length){
replace(node)
}
})
}

replace(fragment)
vm.$el.appendChild(fragment)
}

发布订阅

发布订阅:主要利用数组关系,订阅就是放入函数,发布就是让数组里的函数执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Dep(){
this.subs = [] // 存放函数的事件池
}

Dep.prototype = {
addSub(sub){
this.subs.push(sub)
},
notify(){
this.subs.forEach(sub => sub.update())
}
}

function Watcher(fn){
this.fn = fn
}
Watcher.prototype.update = () => {
this.fn()
}

数据更新视图

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
function replace(frag){
...
node.textContent = txt.replace(reg, val).trim()

new Watcher(vm, RegExp.$1, newVal => {
node.textContent = txt.replace(reg, val).trim()
})
}

function Watcher(vm, exp, fn){
this.fn = fn;
this.vm = vm;
this.exp = exp;
// 添加一个事件
// 这里我们先定义一个属性
Dep.target = this
let arr = exp.split('.')
let val = vm;
arr.forEach(key => { // 取值
val = val[key] // 获取到this.a.b,默认就会调用get方法
});
Dep.target = null
}

function Observe(data) {
let dep = new Dep();
// ...
Object.defineProperty(data, key, {
get() {
Dep.target && dep.addSub(Dep.target); // 将watcher添加到订阅事件中 [watcher]
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
observe(newVal);
dep.notify(); // 让所有watcher的update方法执行即可
}
})
}

Watcher.prototype.update = function() {
// notify的时候值已经更改了
// 再通过vm, exp来获取新的值
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key]; // 通过get获取到新的值
});
this.fn(val); // 将每次拿到的新值去替换{{}}的内容即可
}

参考资料