MVC、MVP、MVVM 有什么区别?前端框架分别更接近哪种?
三者都把界面和数据分开,区别在中间层怎样处理用户输入、怎样更新界面。MVC 由 Controller 接收输入、操作 Model 并选择 View;MVP 由 Presenter 接收 View 转来的事件,Passive View 变体里还由它直接操作界面;MVVM 由 ViewModel 暴露属性和命令,View 通过数据绑定自动同步,ViewModel 不知道 View。Vue 官方说设计部分受 MVVM 启发,React 文档讲的是单向数据流。
简答
三种模式都在回答同一个问题:界面代码和业务数据怎样分开,用户点了按钮以后谁来处理、界面怎样更新。它们都有 Model(业务数据和规则)和 View(界面),区别在中间那一层。
MVC 的中间层是 Controller。按 Microsoft 对 ASP.NET Core MVC 的说明,请求先到 Controller,它操作 Model,再选出要显示的 View,并把数据交给它。MVP 的中间层是 Presenter。Martin Fowler 的描述是,View 上的控件只把用户操作转给 Presenter,由 Presenter 决定怎么反应、去改 Model;在 Passive View 这个变体里,连界面上控件的值都由 Presenter 去设置。MVVM 的中间层是 ViewModel,它暴露属性和命令,View 通过数据绑定显示属性、调用命令,属性变化时发出通知,界面自动更新。Microsoft 的 MVVM 文档特别强调,ViewModel 不知道 View 的存在。
落到前端框架上,Vue 2 官方文档的原话是:Vue 没有严格对应 MVVM,但设计上部分受到它的启发,所以习惯用
vm指代 Vue 实例。React 官方文档没有用这些模式来介绍自己,它讲的是单向数据流,数据从父组件经 props 往下传,子组件通过父组件传下来的回调改状态。要注意,Fowler 说 MVC 是被误解最多的架构模式之一,不同人从不同地方读到的 MVC 并不一样。所以回答时最好说清楚依据的是哪一种描述,不要把某个框架硬说成“就是某某模式”。
三种模式的共同点
先说它们共同的出发点。Microsoft 在 MVVM 文档开头描述了要解决的问题:界面控件和业务逻辑写在一起,改界面的成本高,也很难做单元测试。ASP.NET Core MVC 文档举的例子更具体:界面逻辑比业务逻辑变得更频繁,如果两者混在一个对象里,每改一次界面都要动包含业务逻辑的代码,还要重新测试业务逻辑。
所以三种模式都把 Model 单独拿出来。Model 表示应用的数据和业务规则,不依赖界面,可以独立测试。差别都在 View 和 Model 之间的那一层。
MVC:Controller 接收输入、选择 View
ASP.NET Core MVC 文档对三者的分工是这样写的:用户请求被路由到 Controller;Controller 负责和 Model 一起完成用户的操作或查询,然后选择要显示的 View,并把需要的 Model 数据交给它;View 只负责展示,里面的逻辑应该只和展示内容有关。
依赖方向也写得很清楚:View 和 Controller 都依赖 Model,Model 不依赖 View 也不依赖 Controller。这让 Model 可以脱离界面单独构建和测试。
这是服务端 Web 框架里常见的 MVC。Fowler 介绍的早期桌面 MVC 还有一套 View 通过观察者模式监听 Model 变化的做法,两者并不完全一样,这也是他说 MVC 经常被误解的原因。
MVP:Presenter 接管用户操作
Fowler 对 MVP 的描述是:View 只是一组控件,控件上虽然有事件处理函数,但这些函数只是把控制权交给 Presenter;Presenter 决定怎么响应,去修改 Model。
和 MVC 比,Fowler 指出了两点不同。一是不再把控件拆成 View 和 Controller 两部分;二是 Presenter 通常以整张表单为单位,而不是以单个控件为单位,他认为这一点可能是更大的区别。
MVP 还有两种常见变体。Supervising Controller 里,能用声明方式描述的界面逻辑留在 View,复杂的情况交给 Presenter。Passive View 里,Presenter 负责操作所有控件,View 完全被动。Fowler 提到,Passive View 不在 MVP 最初的描述里,是后来人们为了方便测试发展出来的:View 什么逻辑都没有,测 Presenter 时用一个假的 View 就够了。
MVVM:ViewModel 暴露状态,View 自动绑定
Microsoft 的 MVVM 文档对三者的定义是:View 定义用户在屏幕上看到的结构、布局和外观;ViewModel 实现 View 可以绑定的属性和命令,并通过变更通知事件告诉 View 状态变了;Model 是不可见的类,封装应用的数据和业务、校验逻辑。
关键在依赖方向。文档的原话是,View “知道” ViewModel,ViewModel “知道” Model,但 Model 不知道 ViewModel,ViewModel 也不知道 View。文档还专门提醒,ViewModel 里不要引用按钮、列表这些 View 里的类型,这样 ViewModel 才能单独测试。
这和 MVP 的 Passive View 正好相反。Passive View 里 Presenter 主动去设置控件,Presenter 必须拿着 View 的引用;MVVM 里 ViewModel 只管改自己的属性、发出通知,至于界面怎么更新,交给数据绑定去做。
用一段代码对比 MVP 和 MVVM
下面用纯 JavaScript 模拟一个计数器,不依赖任何框架。先看 MVP:Presenter 拿着 View 的引用,自己调用 view.render()。
const model = { count: 0 };
const fakeView = {
text: '',
render(value) {
this.text = '当前 ' + value;
},
};
function createPresenter(view, data) {
return {
onIncrement() {
data.count += 1;
view.render(data.count);
},
};
}
const presenter = createPresenter(fakeView, model);
presenter.onIncrement();
console.log(fakeView.text); // 当前 1
function createViewModel(data) {
const listeners = [];
return {
get count() {
return data.count;
},
increment() {
data.count += 1;
listeners.forEach((notify) => notify('count'));
},
onPropertyChanged(listener) {
listeners.push(listener);
},
};
}
const viewModel = createViewModel({ count: 0 });
let boundText = '';
viewModel.onPropertyChanged(() => {
boundText = '当前 ' + viewModel.count;
});
viewModel.increment();
viewModel.increment();
console.log(boundText); // 当前 2
console.log(viewModel.count); // 2
两段代码做的事情一样,区别在于谁去更新界面。MVP 里,Presenter 改完数据以后亲自调用 view.render(),所以测试 Presenter 时要传一个假的 View 进去,像这里的 fakeView。
MVVM 里,createViewModel 根本没有 View 参数。它只改数据,再通知“count 变了”;是外面的绑定代码(这里用 onPropertyChanged 模拟)订阅通知、更新 boundText。测试 ViewModel 时直接调用 increment()、读 count 就行,不需要任何界面。真实的 MVVM 框架里,这段绑定代码由框架提供,开发者在模板里声明“这里显示 count”。
前端框架更接近哪一种
先说 Vue。Vue 2 官方文档写的是:Vue 虽然没有严格遵循 MVVM 模式,但设计上部分受到了它的启发,并且习惯用变量 vm(ViewModel 的缩写)指代 Vue 实例。也就是说,组件实例大致扮演 ViewModel,模板是 View,模板里的绑定由框架负责。绑定具体靠响应式系统、模板编译和 v-model 实现,见《Vue 是怎样实现 MVVM 里的数据绑定的?》。
再说 React。React 官方文档没有说自己属于哪种模式。Thinking in React 一文讲的是单向数据流:数据从顶层组件经 props 一层层传到下面;子组件想改数据,要调用父组件传下来的回调。文档还说,这种写法让数据流向更清楚,但比双向数据绑定多写一点代码。所以 React 不提供 MVVM 那种 View 和 ViewModel 之间的双向绑定,不适合直接套用 MVVM 的说法。
因此比较稳妥的回答是:Vue 官方承认受 MVVM 启发;React 官方用单向数据流描述自己;至于“React 是 MVC 里的 V”这类说法,React 官方文档里没有,不要当成定论去讲。
常见追问
MVVM 一定是双向绑定吗?
Microsoft 的 MVVM 文档描述的是 ViewModel 发出属性变更通知、View 通过数据绑定更新,并说 ViewModel 要参与双向绑定,属性就必须发出
PropertyChanged事件。所以双向绑定是 MVVM 里常见的能力,但模式的核心是“ViewModel 不知道 View,靠绑定同步”。Vue 里既有单向的v-bind,也有双向的v-model。
MVP 和 MVVM 哪个更好测试?
两者都把逻辑从 View 里移了出来,都比把逻辑写在界面里好测。区别是 MVP 的 Presenter 要拿着 View 的引用,测试时要准备一个假的 View;MVVM 的 ViewModel 不引用 View,Microsoft 的文档把“可以不用 View 就为 ViewModel 和 Model 写单元测试”列为 MVVM 的好处之一。
为什么说 MVC 是最容易被误解的模式?
Fowler 的原话是,不同的人从不同地方读到 MVC,各自得到不同的理解,都叫它 MVC。比如早期桌面 GUI 里的 MVC 和 ASP.NET Core MVC 这类服务端框架里的 MVC 分工就不一样。所以讨论时先说清楚指的是哪一种。