MVC 和 MVVM 架构模式/设计思想
一、MVC(model View Controller)架构模式
MVC是比较直观的架构模式。
- 视图层(View)是提供给用户的操作界面,是程序的外壳。
- 数据层(Model)是程序需要操作的数据或信息。
- 控制层(Controller)负责根据用户从视图层(View)输入的指令,选取数据层中的数据进行对应的操作,简单来说就是处理业务逻辑的。
执行流程:
用户操作 ➡ View(负责接收用户的输入操作)➡ Controller(业务逻辑处理)➡ Model(数据持久化)➡ View(将结果反馈给View)。
mvc.png
业务逻辑全部分离到 Controller 中,View 只负责展示,Model 只是数据的存储,两者都是相互独立的,模块化程度高。
当业务逻辑变更的时候,不需要变更 View 和 Model,只需要 Controller 换成另一个 Controller,也就达到了解耦和重用的目的。
二、MVVM(Model View ViewModel)架构模式
MVVM 是相对于 MVC 改进的核心思想 。在开发过程中,由于需求的变更或添加,项目的复杂度越来越高,代码量越来越大,我们会发现 MVC 维护起来有点吃力。
由于 Controller 主要是用来处理各种逻辑和数据转化,业务逻辑复杂的 Controller 非常庞大,维护困难。所以有人想到把 Controller 的数据和逻辑处理部分从中抽离出来,用一个专门的对象去管理,这个对象也就是 ViewModel,是 Model 和 View 的一座桥梁。
M 表示模型 Model,V 表示视图 View ,VM 表示数据和模型。
VM 是 MVVM 模式的核心,它是连接 View、Model的桥梁。有两个方向:
-
将模型转换为视图,也就是将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
-
将视图转换为模型,也就是将所看到的页面转化为后端的数据。实现方式是:DOM 事件监听
也就是视图数据的变化会同时修改数据源,而数据源数据的变化也会立即反应到视图上,我们称之为双向绑定。
在 MVVM 的框架下视图和模型是不能直接通信的。它们通过 ViewModel 来通信,ViewModel 通常要实现一个 observer 观察者,当数据发生改变,ViewModel 能够监听到数据的这种变化,然后通知到对应的视图做自动更新。而当用户操作视图,ViewModel 也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。
三、观察者 - 订阅者(数据劫持)
Vue observer 数据监听器,把一个普通的 JavaScript 对象传给了 Vue 实例的 data 选项,Vue 将遍历此对象所有属性,并使用 Object.defineProperty() 方法把这些属性全部转成 setter、getter 方法。当 data 的某个属性被访问的时,则会调用 getter 方法,当 data 中的属性被改变时,则会调用 setter 方法。
observer.png当执行 new Vue() 时,Vue 就进入初始化阶段。一方面 Vue 会遍历 data 选项中的属性,并用 Object.defineProperty() 将它们转为 getter/setter,实现数据变化监听功能。另一方面,Vue 的指令编译器 complie 对元素节点的指令进行解析,初始化视图,并订阅 Watcher 来更新视图,此时 watcher 会将自己添加到消息订阅器(Dep)初始化完毕。当数据发生变化时,observer 中的 setter 方法被触发,setter 会立即调用 Dep.notify(),Dep 开始遍历的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行响应的更新。
四、实现一个简单 Vue 双向绑定
新建一个文件,名为 myvue.html,大致如下所示:
<body>
<div id="app">
我是{{message}}内容
<div>
{{message}}
</div>
<span>
<span>{{message}}</span>
</span>
{{bindData}}
</div>
</body>
<script>
let vm = new Vue({
el: '#app',
data: {
message: '测试数据',
bindData: '绑定的数据'
}
})
</script>
接下来我们实现一个 Vue 的双向绑定。
class Vue extends EventTarget {
constructor(options) {
super()
this.$options = options;
this.observer(this.$options.data);
this.compile();
}
// 数据监听器
observer(data) {
let keys = Object.keys(data);
keys.forEach(key => {
let value = data[key];
// 订阅器
let dep = new Dep();
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
// 获取
get() {
// 收集订阅者
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
// 设置更新
set(newValue) {
// 更新
dep.notify(newValue);
if (newValue !== value) {
value = newValue;
}
}
})
});
}
// 指令编译器
compile() {
let ele = document.querySelector(this.$options.el);
this.compileNode(ele);
}
// 子节点
compileNode(ele) {
let childNodes = ele.childNodes;
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 如果是元素节点找到所有
this.compileNode(node);
} else if (node.nodeType === 3) {
// 文本节点
let reg = /\{\{\s*(\S+)\s*\}\}/g;
if (reg.test(node.textContent)) {
let $1 = RegExp.$1; // 匹配
// 替换
node.textContent = node.textContent.replace(reg, this.$options.data[$1]);
// 订阅者
new Watcher(this.$options.data, $1, (newValue) => {
let oldValue = this.$options.data[$1];
reg = new RegExp(oldValue, 'g');
node.textContent = node.textContent.replace(reg, newValue);
});
};
};
});
}
};
// 订阅管理器
class Dep {
constructor() {
this.subs = [];
}
// 存储
addSub(sub) {
this.subs.push(sub);
}
// 更新
notify(newValue) {
this.subs.forEach(sub => {
sub.update(newValue);
})
}
}
// 订阅者
class Watcher {
constructor(data, key, cb) {
Dep.target = this;
this.cb = cb;
data[key];
Dep.target = null;
}
update(newValue) {
this.cb(newValue)
}
}
五、Vue2.0 使用 defineProperty 存在缺陷,Vue3.0 已改为使用 proxy
- Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
- Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
六、MVC 和 MVVM 的区别
MVC 和 MVVM 的区别并不是VM完全取代了C,ViewModel 存在目的在于抽离Controlle r中展示的业务逻辑,而不是替代 Controller,其它视图操作业务等还是应该放在 Controller 中实现。也就是说 MVVM 实现的是业务逻辑组件的重用。由于 MVC 出现的时间比较早,前端并不那么成熟,很多业务逻辑也是在后端实现,所以前端并没有真正意义上的 MVC 模式。而我们今天再次提起 MVC ,是因为大前端的来到,出现了 MVVM 模式的框架,我们需要了解一下 MVVM 这种设计模式是如何一步步演变过来的。