MVC、MVP、MVVM:用户界面与业务逻辑的解耦
MVC、MVP、MVVM是前端开发中经典的软件架构模式,通过关注点分离实现用户界面与业务逻辑解耦,提升代码可维护性与可扩展性。
一、MVC:分层解耦,实现初步解耦
![]()
核心思想
将代码划分为Model(数据 / 逻辑)、View(界面 / 交互)、Controller(协调 / 控制)三大组件,每个组件有特定的职责。
组件责职
| 组件 | 职责说明 | 关键特点 |
|---|---|---|
| Model | 数据管理与业务逻辑。包括数据的访问、处理和存储。 | 不关心数据展示;当数据发生变化时,通知View更新 |
| View | 界面展示与用户交互。从Model获取数据进行展示,同时将用户输入传递给Controller。 | 一个模型可以对应多个视图,支持不同的展示方式 |
| Controller | 协调View和Model。接收用户输入,调用 Model 处理数据,指定View进行渲染 | 扮演“中间协调者”角色 |
说明:在Web早期,用户交互主要是用户访问超链接或表单提交,服务器返回新的HTML页面。随着Ajax技术的出现,前后端分离,前端异步请求数据,后端返回JSON数据,由前端进行局部渲染更新页面。
交互流程
- 用户与View交互,View将用户输入传递给Controller。
- Controller调用Model处理数据。
- Model执行完业务逻辑并返回数据。
- Controller选择View返回给用户,View从Model获取数据生成HTML或JSON。
典型框架
- Spring MVC
Spring MVC是一个经典的Java Web框架,一个请求处理流程如下:
![]()
- Backbone.js
早期的MVC框架主要应用在服务器端,随着前后端分离,前端也有自己的MVC框架。Backbone.js就是其中之一。
在前后端分离的架构下,前端主要做两件事情:视图展示和前后端数据同步,对应Backbone. js中两大组件:Model 和 View。
Model负责数据管理与业务逻辑,并通过RESTful API与后端同步数据。当数据变化时,Model会触发事件通知视图更新。
View监听Model变化,从Model获取数据进行展示;监听DOM事件,接收用户输入并调用Model处理数据。
Backbone.js的View比较厚,承担了MVC中的View和Controller的职责。
// Model定义
var User = Backbone.Model.extend({
urlRoot: '/api/users'
});
// View定义
var UserView = Backbone.View.extend({
initialize: function() {
// 手动监听模型变化
this.listenTo(this.model, 'change', this.render);
},
render: function() {
// 手动构建HTML或使用模板引擎
this.$el.html(`<h1>${this.model.get('name')}</h1>
<p>Email: ${this.model.get('email')}</p>
<button class="edit-btn">编辑</button>`);
return this;
},
events: {
'click .edit-btn': 'editUser'
},
editUser: function() {
// 手动创建编辑视图
var editView = new UserEditView({model: this.model});
$('#modal-container').html(editView.render().el);
}
});
// Controller控制流程
var user = new User({id: 123});
user.fetch(); // 从服务器获取数据
var userView = new UserView({model: user, el: '#user-container'});
userView.render(); // 初始渲染
补充:Backbone.js是早期的SPA开发框架,后来被Vue、React逐步代替。
MVC的优缺点
| 优点 | 缺点 |
|---|---|
| 修改View/Model互不影响 | View依赖Model(需监听Model变化并获取数据) |
| View和Model支持并行开发 | Controller职责过重,需要协调View/Model |
| 一个Model可以对应多个View,支持不同的展示方式。 | 需要手动同步View和Model |
| 可独立测试 View/Model |
二、MVP:Presenter代理,实现彻底解耦
![]()
核心思想
用 Presenter 代替 Controller,彻底切断 View 与 Model 的直接通信。通过Presenter作为双向代理,实现两者的间接通信,解耦更彻底。
组件职责
| 组件 | 关键变化 |
|---|---|
| View | 仅提供渲染接口给Presenter调用,不需要从Model获取数据进行展示 |
| Model | 数据变化通知 Presenter 而非 View |
| Presenter | 接收用户输入 → 调用 Model接口处理数据 → 调用 View 接口更新界面 |
交互流程
- 用户与View交互,View调用Presenter接口处理事件。
- Presenter调用Model处理数据。
- Model完成后回调Presenter。
- Presenter调用View接口更新界面。
典型框架
MVP框架主要应用在安卓界面开发,示例代码如下:
class Todo {
private content: string;
constructor(content: string) {
this.content = content;
}
getContent(): string {
return this.content;
}
}
interface ITodoModel {
interface TodosCallback {
onSuccess(): void;
}
saveTodo(todo: Todo, callback: GetTodosCallback): void;
}
interface ITodoView {
showSuccess(): void;
}
interface ITodoPresenter {
saveTodo(todo: Todo):void;
}
class TodoModel implement ITodoModel {
saveTodo(todo: Todo, callback: GetTodosCallback): void {
saveDB(todo);// 模拟保存数据库
callback.onSuccess();
}
}
class BasePresenter<T> {
private mView: T;
attachView(view: T):void {
mView = view;
}
dettachView():void {
mView = null;
}
getView():T {
return mView;
}
}
class TodoPresenter extends BasePresenter<ITodoView> implement ITodoModel.TodosCallbackI, TodoPresenter {
todoModel: ITodoModel;
constructor(){
this.todoModel = new TodoModel();
}
saveTodo(todo: Todo):void {
this.todoModel.saveTodo(todo, this);
}
onSuccess() {
getView().showSuccess();
}
}
TodoActivity extends AppCompatActivity implements ITodoView {
private TextView mResultTv;
private EditText mTodoContentEt;
private TodoPresenter mTodoPresenter;
onCreate(Bundle savedInstanceState):void {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_todo);
initPresenter();
initView();
}
onDestroy(): void {
super.onDestroy();
this.mTodoPresenter.dettachView();
}
initView():void {
this.mResultTv = findViewById(R.id.tv_result);
this.mTodoContentEt = findViewById(R.id.et_todo_content);
findViewById(R.id.btn_add).setOnClickListener(new View.OnClickListener() {
onClick(View view):void {
const todo = getTodoInput();
this.mTodoPresenter.saveTodo(todo);
}
});
}
initPresenter() {
this.mTodoPresenter = new TodoPresenter();
this.mTodoPresenter.attachView(this);
}
getTodoInput(): Todo {
const content = mTodoContentEt.getText().toString();
return new Todo(content);
}
showSuccess(): void {
this.mResultTv.setText("success");
}
}
解读:
- ITodoModel的职责是数据管理,并通过回调通知ITodoPresenter。
- ITodoView的职责是展示数据,调用ITodoPresenter接口处理用户输入。
- ITodoPresenter负责接收用户输入,调用ITodoModel处理数据,然后调用ITodoView展示数据。
MVP的优缺点
| 优点 | 缺点 |
|---|---|
| View 与 Model 完全解耦 | Presenter职责过重,需要组织View/Model完成所有交互 |
| 基于接口通信,进一步解耦,可独立测试 | 需定义大量接口,增加代码复杂度 |
| View 逻辑纯粹(仅负责渲染) | 仍需要手动同步View和Model |
三、MVVM:数据绑定,实现自动同步
![]()
核心思想
通过数据绑定实现 View 与 Model 的自动同步,减少手动DOM操作。
组件职责
| 组件 | 职责说明 |
|---|---|
| ViewModel | 响应式系统:监听 Model 变化并自动同步到 View;监听 DOM 事件并自动更新 Model。 |
| View | 声明式地绑定关联的Model,无需手动操作 DOM。 |
| Model | 数据管理与业务逻辑,不关系数据展示。 |
交互流程
- 用户与View交互,ViewModel自动将用户输入同步给Model。
- Model执行业务逻辑和操作数据,更新状态。
- Model变化后,ViewModel自动将Model同步到View。
典型框架
2010年后,Vue.js和React等主流框架采用MVVM架构,减少了DOM操作。
<body>
<div id="app">
常见搜索关键字:<span @click="setKeyword('人工智能')">人工智能</span>
<input
v-model="keyword"
placeholder="请输入要搜索的关键字..."
@keyup.enter="search"
>
<button @click="search">
搜索
</button>
<div v-if="loading">
正在搜索中,请稍候...
</div>
<div v-for="result in searchResults" :key="result.id">
<div>{{ result.content }}</div>
</div>
</div>
</body>
<script>
const { createApp, ref, computed } = Vue;
createApp({
setup() {
const keyword = ref('');
const loading = ref(false);
const searchResults = ref([]);
// 模拟数据
const allResults = ref([
{
id: 1,
content: 'Vue.js 入门教程',
},
{
id: 2,
content: 'JavaScript 高级编程',
},
]);
const setKeyword = (example) => {
keyword.value = example;
search();
};
const search = () => {
const searchTerm = keyword.value.toLowerCase();
searchResults.value = allResults.value.filter(result => (
result.content.toLowerCase().includes(searchTerm));
);
};
return {
keyword,
setKeyword,
loading,
searchResults,
search,
};
}
}).mount('#app');
</script>
解读:
- 在Vue.js中使用v-model指令声明式地实现双向绑定。当用户输入关键字时,数据会自动同步到keyword变量。反之,当用户点击“常见搜索关键字” 按钮,改变keyword变量的值,也会自动同步到输入框。
- 在Vue.js模版中使用{{}}声明式地实现单向绑定。当执行完搜索逻辑并把结果赋值给searchResults变量,数据会被自动渲染到结果列表。
MVVM的优缺点
| 优点 | 缺点 |
|---|---|
| 自动同步View和Model,减少手动 DOM 操作 | 数据绑定可能带来性能开销 |
| ViewModel 可独立测试 | 双向绑定,调试复杂度高 |
四、总结
| 架构 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| MVC | 分层解耦 | 职责分离,易于扩展 | View依赖Model | 简单应用、后端开发 |
| MVP | 代理隔离 | 彻底切断 View 与 Model 关联 | 需要手动同步View和Model | 中大型应用、需严格测试 |
| MVVM | 数据绑定 | 实现View和Model自动同步 | 有一定的性能与调试成本 | 现代前端应用、SPA 开发 |