MVVM原理

本文最后更新于 2025年8月4日 晚上

参考:https://juejin.cn/post/6844903586103558158

现在无论是 React 还是 Vue 还是最新的 Angular,MVVM 双向数据绑定通过 数据劫持+发布订阅模式 完成。真正实现其实靠的是 ES5 中提供的 Object.defineProperty() 方法。

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

Object.defineProperty(obj, "music", {
value: "七里香", // 1.属性值
configurable: true, // 2.属性是否可被删除
writable: true, // 3.属性值是否可修改
enumerable: true, // 4.属性是否可被枚举
// get 和 set 设置时不能设置 writable 和 value,它们代替了二者且是互斥的
set(val) {
song = val;
},
get() {
return song;
},
});

// 下面打印的部分分别是对应代码写入顺序执行
// 1
console.log(obj); // {singer: '周杰伦', music: '七里香'}
// 2
delete obj.music;
console.log(obj); // {singer: '周杰伦'}
// 3
obj.music = "听妈妈的话";
console.log(obj); // {singer: '周杰伦', music: "听妈妈的话"}
// 4
for (let key in obj) {
console.log(key); // singer, music
}
// 5
console.log(obj.music); // '发如雪'
// 6
obj.music = "夜曲";
console.log(obj.music); // '夜曲'

打造 MVVM

以 Vue 为参照去实现 MVVM。

1
2
3
4
5
6
7
8
9
// 创建一个 Mvvm 构造函数
function Mvvm(options = {}) {
// 将所有属性挂载到 $options
this.$options = options;
// this._data 也和 Vue 一样
let data = (this._data = this.$options.data);
// 数据劫持
Observe(data);
}

数据劫持

为什么要做数据劫持?

  • 观察对象,给对象增加 Object.defineProperty()
  • vue 特点是不能新增不存在的属性 不存在的属性没有 get 和 set
  • 深度响应 因为每次赋予一个新对象时会给这个新对象增加 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
// 创建一个 Observe 构造函数写数据劫持的主要逻辑
// 数据劫持就是给对象增加 get 和 set
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 (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
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[key] = 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中的内容放入内存中
}
// 对el里面的内容进行替换
function replace(frag) {
Array.from(frag.childNodes).forEach((node) => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}}
if (node.nodeType === 3 && reg.test(txt)) {
// 即是文本节点又有大括号的情况{{}}
console.log(RegExp.$1); // 匹配到的第一个分组 如: a.b, c
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

数据更新视图

1

双向数据绑定

1


MVVM原理
https://xuekeven.github.io/2021/09/20/MVVM原理/
作者
Keven
发布于
2021年9月20日
更新于
2025年8月4日
许可协议