前端软件架构模式mvc/mvp/mvvm

MVC,MVP和MVVM都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。
要了解MVC、MVP和MVVM,就要知道它们的相同点和不同点。不同部分是C(Controller)、P(Presenter)、VM(View-Model),而相同的部分则是MV(Model-View)。

  • m: 模型(Model)
  • v: 视图(View)
  • c: 控制器(Controller)接收v的事件,改变m
  • p: View和Model之间的“中间人”,需手动更新
  • vm: 替换P,实现自动更新

MVC

MVC模式(Model–view–controller): 是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

数据关系

  • View 接受用户交互请求
  • View 将请求转交给Controller
  • Controller 操作Model进行数据更新
  • 数据更新之后,Model通知View更新数据变化。PS: 还有一种是View作为Observer监听Model中的任意更新,一旦有更新事件发出,View会自动触发更新以展示最新的Model状态。这种方式提升了整体效率,简化了Controller的功能,不过也导致了View与Model之间的紧耦合。
  • View 更新变化数据

数据流




Model和View可能有耦合,即MVC仅仅将应用抽象,并未限制数据流。


MVP

MVP(Model-View-Presenter)是MVC模式的改良。MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

数据关系

  • View 接收用户交互请求
  • View 将请求转交给 Presenter
  • Presenter 操作Model进行数据更新
  • Model 通知Presenter数据发生变化
  • Presenter 更新View数据

数据流

MVP模式限制了Model和View之间通信,让Model和View解耦更彻底,代码更容易被复用。MVP模式也有问题,它的问题在于Presenter的负担很重,Presenter需要知道View和Model的结构,并且在Model变化时候需要手动操作View,增加编码负担,降低代码维护性。


MVVM

MVVM(Model-View-ViewModel) 在 MVVM 中,不需要Presenter手动地同步View和Model。View 是通过数据驱动的,Model一旦改变就会相应的刷新对应的 View,View 如果改变,也会改变对应的Model。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。

数据关系

  • View 接收用户交互请求
  • View 将请求转交给ViewModel
  • ViewModel 操作Model数据更新
  • Model 更新完数据,通知ViewModel数据发生变化
  • ViewModel 更新View数据

数据流

从上面对MVC、MVP、MVVM的描述可以看出,它们是递进关系,不断优化的:MVC中Model和View还有一定程度的耦合,而在MVP和MVVM中View和Model彻底分离,View和Model不知道彼此的存在,View和Model只向外暴露方法让Presenter调用;MVVM通过自动同步数据更新到视图,解决了MVP中手动同步的痛点,简化了代码。


区别总结

MVVM是一种软件架构设计模式,它抽离了视图、数据和逻辑,并限定了Model和View只能通过VM进行通信,VM订阅Model并在数据更新时候自动同步到视图。
MVC将应用抽象为数据层(Model)、视图层(View)、逻辑层(controller),降低了项目耦合。但MVC并未限制数据流,Model和View之间可以通信。
MVP则限制了Model和View的交互都要通过Presenter,这样对Model和View解耦,提升项目维护性和模块复用性。
而MVVM是对MVP的P的改造,用VM替换P,将很多手动的数据=>视图的同步操作自动化,降低了代码复杂度,提升可维护性。

写法示例

1、原生

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>origin</title>
</head>
<body>
<div id="root"></div>
<script src="origin.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// origin.js
var isShow = true;

var modal = document.createElement('div');
var switchButton = document.createElement('button');
switchButton.innerText = '关';
switchButton.onclick = function () {
if (isShow) {
switchButton.innerText = '开';
isShow = false;
}
else {
switchButton.innerText = '关';
isShow = true;
}
}
modal.appendChild(switchButton);
document.getElementById('root').appendChild(modal);

2、MVC

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
// mvc.js
class View {
constructor(controller) {
this.controller = controller;
}
render(model) {
const button = document.createElement('button');
const isOpen = model.getState().isOpen;
button.innerText = isOpen ? '关' : '开';
button.onclick = this.controller.switch;

const root = document.getElementById('root');
root.innerHTML = '';
root.appendChild(button);
return button;
}
};

class Model {
state = {
isOpen: false
};

_views = [];

getState() {
return this.state;
}

register(view) {
this._views.push(view);
}

switch() {
this._update({
isOpen: !this.state.isOpen
});
}

_update(data) {
Object.assign(this.state, data);
this._notify();
}

_notify() {
this._views.forEach(view => view.render(this));
}
};

class Controller {
constructor() {
this._view = new View(this);
this._model = new Model();
this._model.register(this._view);
this._view.render(this._model);
}
switch = () => {
this._model.switch();
console.log('switch!!!');
};
}

const controller = new Controller();

3、MVP

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
// mvp.js
class View {
constructor(presenter) {
this.presenter = presenter;
}
// view不需要关注model的数据结构,只关注自己需要的数据
// 这样也不会存在view使用了model的其他数据的情况
render({isOpen}) {
const button = document.createElement('button');
button.innerText = isOpen ? '关' : '开';
button.onclick = this.presenter.switch;

const root = document.getElementById('root');
root.innerHTML = '';
root.appendChild(button);
return button;
}
};

// model不需要在数据更新时候触发视图更新,只负责数据存储
class Model {
state = {
isOpen: false
};

getState() {
return this.state;
}

switch() {
this.state.isOpen = !this.state.isOpen;
}
};

// persenter需要知道m和v的结构,并且要在数据改变时候更新视图,还要处理所有的交互逻辑
class Presenter {
constructor() {
this._view = new View(this);
this._model = new Model();

const {isOpen} = this._model.getState();
this._view.render({isOpen});
}
switch = () => {
this._model.switch();

const {isOpen} = this._model.getState();
this._view.render({isOpen});

console.log('switch!!!');
};
}

const presenter = new Presenter();

4、MVVM

1
2
3
4
5
6
7
8
9
10
11
12
13
// mvvm.js
var app = new Vue({
el: '#root',
template: `<button @click="onSwitch()">{{isOpen ? '关' : '开'}}</button>`,
data: {
isOpen: false
},
methods: {
onSwitch() {
this.isOpen = !this.isOpen;
}
}
});