简介

MVC,MVP和MVVM都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。不同于设计模式(Design Pattern),只是为了解决一类问题而总结出的抽象方法,一种架构模式往往使用了多种设计模式。 —— 参考文章【1】

它们目标都是解耦,解耦好处一个是关注点分离,提升代码可维护和可读性,并且提升代码复用性。

它们都将应用抽象分离成视图、逻辑、数据3层。

MVC将应用抽象为数据层(Model)、视图(View)、逻辑(Controller),这样数据、视图、逻辑的代码各自汇聚。各层之间的通信模式则没有严格限制,在实际开发中也有各种实现(摘自文章【2】中的示意图),有这样的,

MVVM - 图1

也有这样的

MVVM - 图2

还有这样的

MVVM - 图3

但有一点可以确定的是,在MVC模式中,Model和View可能有耦合,即MVC仅仅将应用抽象,并未限制数据流

MVP则在MVC基础上,限定了通信方式,即Model和View之间不直接通信,都通过Presenter通信,这个Presenter和MVC中的Controller一脉相承,代表应用中的逻辑层。Presenter负责项目中的逻辑,并且直接与View和Model通信,操作数据更新更新后手动同步到View上。

MVVM - 图4

MVP模式限制了Model和View之间通信,让Model和View解耦更彻底,代码更容易被复用。

MVP模式也有问题,它的问题在于Presenter的负担很重,Presenter需要知道View和Model的结构,并且在Model变化时候需要手动操作View,增加编码负担,降低代码维护性。

于是MVVM设计了VM层,即ViewModel层,ViewModel自动同步数据到视图,用VM代替P之后,MVVM自动从Model映射到View(实现方式是模板渲染),不需要用户手动操作视图,这样代码更简单不易出错,代码更好阅读和维护。

MVVM - 图5

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

总结

如果面试中被问到:MVC、MVP和MVVM之间的区别是什么?可以这么回答:

MVC将应用抽象为数据层(Model)、视图层(View)、逻辑层(controller),降低了项目耦合。但MVC并未限制数据流,Model和View之间可以通信。

MVP则限制了Model和View的交互都要通过Presenter,这样对Model和View解耦,提升项目维护性和模块复用性。

而MVVM是对MVP的P的改造,用VM替换P,将很多手动的数据=>视图的同步操作自动化,降低了代码复杂度,提升可维护性。

那么什么是MVVM?MVVM是一种软件架构设计模式,它抽离了视图、数据和逻辑,并限定了Model和View只能通过VM进行通信,VM订阅Model并在数据更新时候自动同步到视图。

示例

下面我们通过一个简单的例子来展示不同的模式。

要实现的效果很简单,就是一个开关按钮,点击会切换开关状态状态。

MVVM - 图6

1. 原生写法

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>origin</title>
  6. </head>
  7. <body>
  8. <div id="root"></div>
  9. <script src="origin.js"></script>
  10. </body>
  11. </html>
  1. // origin.js
  2. var isShow = true;
  3. var modal = document.createElement('div');
  4. var switchButton = document.createElement('button');
  5. switchButton.innerText = '关';
  6. switchButton.onclick = function () {
  7. if (isShow) {
  8. switchButton.innerText = '开';
  9. isShow = false;
  10. }
  11. else {
  12. switchButton.innerText = '关';
  13. isShow = true;
  14. }
  15. }
  16. modal.appendChild(switchButton);
  17. document.getElementById('root').appendChild(modal);

2. MVC

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>mvc</title>
  6. </head>
  7. <body>
  8. <div id="root"></div>
  9. <script src="mvc.js"></script>
  10. </body>
  11. </html>
  1. // mvc.js
  2. class View {
  3. constructor(controller) {
  4. this.controller = controller;
  5. }
  6. render(model) {
  7. const button = document.createElement('button');
  8. const isOpen = model.getState().isOpen;
  9. button.innerText = isOpen ? '关' : '开';
  10. button.onclick = this.controller.switch;
  11. const root = document.getElementById('root');
  12. root.innerHTML = '';
  13. root.appendChild(button);
  14. return button;
  15. }
  16. };
  17. class Model {
  18. state = {
  19. isOpen: false
  20. };
  21. _views = [];
  22. getState() {
  23. return this.state;
  24. }
  25. register(view) {
  26. this._views.push(view);
  27. }
  28. switch() {
  29. this._update({
  30. isOpen: !this.state.isOpen
  31. });
  32. }
  33. _update(data) {
  34. Object.assign(this.state, data);
  35. this._notify();
  36. }
  37. _notify() {
  38. this._views.forEach(view => view.render(this));
  39. }
  40. };
  41. class Controller {
  42. constructor() {
  43. this._view = new View(this);
  44. this._model = new Model();
  45. this._model.register(this._view);
  46. this._view.render(this._model);
  47. }
  48. switch = () => {
  49. this._model.switch();
  50. console.log('switch!!!');
  51. };
  52. }
  53. const controller = new Controller();

3. MVP

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>mvp</title>
  6. </head>
  7. <body>
  8. <div id="root"></div>
  9. <script src="mvp.js"></script>
  10. </body>
  11. </html>
  1. // mvp.js
  2. class View {
  3. constructor(presenter) {
  4. this.presenter = presenter;
  5. }
  6. // view不需要关注model的数据结构,只关注自己需要的数据
  7. // 这样也不会存在view使用了model的其他数据的情况
  8. render({isOpen}) {
  9. const button = document.createElement('button');
  10. button.innerText = isOpen ? '关' : '开';
  11. button.onclick = this.presenter.switch;
  12. const root = document.getElementById('root');
  13. root.innerHTML = '';
  14. root.appendChild(button);
  15. return button;
  16. }
  17. };
  18. // model不需要在数据更新时候触发视图更新,只负责数据存储
  19. class Model {
  20. state = {
  21. isOpen: false
  22. };
  23. getState() {
  24. return this.state;
  25. }
  26. switch() {
  27. this.state.isOpen = !this.state.isOpen;
  28. }
  29. };
  30. // persenter需要知道m和v的结构,并且要在数据改变时候更新视图,还要处理所有的交互逻辑
  31. class Presenter {
  32. constructor() {
  33. this._view = new View(this);
  34. this._model = new Model();
  35. const {isOpen} = this._model.getState();
  36. this._view.render({isOpen});
  37. }
  38. switch = () => {
  39. this._model.switch();
  40. const {isOpen} = this._model.getState();
  41. this._view.render({isOpen});
  42. console.log('switch!!!');
  43. };
  44. }
  45. const presenter = new Presenter();

3. MVVM

使用vue示例

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>mvvm</title>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  7. </head>
  8. <body>
  9. <div id="root"></div>
  10. <script src="mvvm.js"></script>
  11. </body>
  12. </html>
  1. // mvvm.js
  2. var app = new Vue({
  3. el: '#root',
  4. template: `<button @click="onSwitch()">{{isOpen ? '关' : '开'}}</button>`,
  5. data: {
  6. isOpen: false
  7. },
  8. methods: {
  9. onSwitch() {
  10. this.isOpen = !this.isOpen;
  11. }
  12. }
  13. });

参考文章

MVC、MVP、MVVM的区别和联系(精讲版)【1】

MVC,MVP 和 MVVM 的图示 - 阮一峰的网络日志【2】

浅析前端开发中的 MVC/MVP/MVVM 模式【3】