手写 MVVM

对于MVVM的实现原理之前其实就知道了,但是一直没有自己去尝试实现过,今个在这里实现一下,也加深下印象

参考文章:MVVM原理

入口函数

1
2
3
4
5
6
7
8
9
10
// 默认配置项为空
function Mvvm(options = {}){
// vm.$options Vue上是将所有属性挂在到上面
this.$options = options;
// this._data 这里也和Vue一样
let data = this._data = this.$options.data;

// 对data进行数据劫持
observe(data);
}

数据劫持

  • 观察对象,遍历属性调用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
function Observe(data){
// 遍历劫持
for(let key in data){
let val = data[key];
observe(val); // 递归查找添加劫持
Object.defineProperty(data, key, {
configurable: true, // 表示可删除属性
get(){
return val;
},
set(newVal){
if(newVal === val) return;
val = newVal;
observe(newVal); // 对新加入的数据进行劫持
}
})
}
}

// 减少new
function observe(data){
if(!data || typeof data !== 'object') return;
return new Observe(data);
}

数据代理

数据代理就是不需要我们写成mvvm._data.a.b这种形式,而是mvvm.a.b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 完善Mvvm入口函数
function Mvvm(options = {}){
// 数据劫持
observe(data);
// this 代理了this._data
for(let key in data){
Object.defineProperty(this, key, {
configurable:true,
get(){
return this._data[key];
},
set(newVal){
this._data = newVal;
}
})
}
}

数据编译

在数据劫持和数据代理都实现后,还需要将{{}}里面的内容解析出来

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 = {}){
observe(data);

new Compile(options.el, this);
}

// 创建Compile构造函数
function Compile(el, vm){
// 挂载el到实例上
vm.$el = document.querySelector(el);
// 在el范围里将内容都拿到,可以移到内存中去然后放入文档碎片中,节省开销
let fragment = document.createDocumentFragment();

while(child = vm.$el.firstChild){
fragment.appendChild(child);
}
// 对el里面的内容进行替换
function replace(frag){
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g;

if(node.nodeType === 3 && reg.test(txt)){ // 即是文本节点又有大括号的情况{{}}
let arr = RegExp.$1.split('.');
let val = vm;
arr.forEach(key => {
val = val[key]; // this.a.b
});
// 用trim方法去除首位空格
node.textContent = txt.replace(reg, val).trim();
}

// 如果还有子节点,继续递归replace
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment); // 替换内容

vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}

发布订阅

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
// 发布订阅模式  订阅和发布 如[fn1, fn2, fn3]
function Dep() {
// 一个数组(存放函数的事件池)
this.subs = [];
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
notify() {
// 绑定的方法,都有一个update方法,通过update方法调用queryWatcher,去异步更新
this.subs.forEach(sub => sub.update());
}
};
// 监听函数
// 通过Watcher这个类创建的实例,都拥有update方法
function Watcher(fn) {
this.fn = fn; // 将fn放到实例上
}
Watcher.prototype.update = function() {
this.fn();
};

let watcher = new Watcher(() => console.log(111)); //
let dep = new Dep();
dep.addSub(watcher); // 将watcher放到数组中,watcher自带update方法, => [watcher]
dep.addSub(watcher);
dep.notify(); // 111, 111

数据更新视图

  • 订阅一个事件,当数据改变需要重新刷新视图,这就需要在replace替换的逻辑里来处理
  • 通过new Watcher把数据订阅一下,数据一变就执行改变内容的操作
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
function replace(frag){
// 省略...
// 替换的逻辑
node.text.Content = txt.replace(reg, val).trim();

// 监听变化
// 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参
new Watcher(vm, RegExp, $1, newVal => {
node.textContent = txt.replace(reg, newVal).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;
}

由于数据劫持的原因,当获取值的时候就会自动调用get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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方法执行即可
}
})
}

当set修改值的时候执行了dep.notify方法,这个方法是执行watcher的update方法,需要对update进行修改

1
2
3
4
5
6
7
8
9
10
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); // 将每次拿到的新值去替换{{}}的内容即可
};

mounted函数

mounted函数实际就是在所有响应式数据处理完,即compile函数执行之后执行的

computed函数

在compile函数执行前调用,会从options属性里获取computed对象,之后遍历key值设置响应式。如果当前key对应的value是对象,则需要手动调用get方法,如果是函数则不需要手动调用

作者

徐云飞

发布于

2022-11-18

更新于

2023-02-05

许可协议

评论