深入不出 — javaScript设计模式(五):MVC/MVP/MVVM

1,333 阅读5分钟

写在前面

生活中的很多事做多了熟练以后经常能结出一些套路,使用套路能让事情的处理变得容易起来。而设计模式就是写代码的套路,好比剑客的剑谱,说白了就是一个个代码的模板。

就算从没看过设计模式的人工作中肯定多多少少也接触或不经意间就使用过某种设计模式。因为代码的原理都是一样的,实现某个需求的时候可能自然而然就以最佳的模式去实现了。

这些模式已经有前人做了整理总结,不仅可以直接拿来使用,在阅读开源库源码时也会发现这些库都大量的使用了设计模式。

系列文章

深入不出 — javaScript设计模式(一):创建型设计模式
深入不出 — javaScript设计模式(二):结构型设计模式
深入不出 — javaScript设计模式(三):行为型设计模式
深入不出 — javaScript设计模式(四):技巧型设计模式
深入不出 — javaScript设计模式(五):架构型设计模式

本系列内容主要来自于对张容铭所著《JavaScript设计模式》一书的理解与总结(共5篇),由于文中有我自己的代码实现,并使用了部分新语法,抛弃了一些我认为繁琐的内容,甚至还有对书中 "错误" 代码的修正。所以如果发现我理解有误,代码写错的地方麻烦务必指出!非常感谢!

前置知识

掌握javaScript基础语法,并对js原理(特别是原型链)有较深理解。

架构型设计模式

架构型设计模式是一类框架结构,通过提供一些子系统,指定他们的职责,并将他们条理清晰的组织在一起。

由于js在ES6之后,从语言标准的层面上实现了模块功能。

并且如今前端形成使用vue/react等单页应用框架(组件化视图开发)配合npm/yarn(模块管理)和webpack/vite(打包构建)等工具实现的工程化的主流开发模式。

本章中所讲述的模块化模式与Widget模式(组件化视图)意义已经不大。建议使用标准的ESModule模块化开发,通过vue等框架学习组件化视图开发。

前端的MVC、MVP、MVVM

关于这三种架构模式在前端应用中的含义与区别,我综合书本以及资料查阅,个人总结理解如下。

首先MVC模式是三个模式中的基础,也就是说MVP和MVVM都是基于MVC模式衍生出来的,MVP与MVVM都可以看做应用MVC思想做了部分改动的模式。

MVC

所谓MVC就是将软件分为三个部分:

  • 模型(Model):数据保存
  • 视图(View):用户界面
  • 控制器(Controller):业务逻辑

三个部分间的通信是单向的。

image

比如页面上显示一个数字和两个按钮(加/减),对应到MVC三个部分:

  1. 用户点击页面(View)上的增加按钮。
  2. View将指令传达到Controller
  3. Controller根据业务逻辑(增加)指挥数字(Model)加一。
  4. Model发生变化后将新的数字发送到View,页面上用户看到数字加一。
<div>数字:<span id="num">0</span></div>

<input id="add" type="button" value="加">
<input id="subtract" type="button" value="减">
const App = {
  Model: function() {
    this.num = 0

    this.changeNum = function(num) {
      this.num = num
      // 通知视图
      this.noticeObserves()
    }
    
    // 观察者模式数据变化时通知View
    this.Observes = []
  
    this.addObserve = function(observe) {
      this.Observes.push(observe)
    }

    this.noticeObserves = function() {
      this.Observes.forEach(ob => {
        ob.renderView()
      })
    }
  },

  View: function(controller, model) {
    const numDom = document.getElementById('num')

    const addBtn = document.getElementById('add')
    const subtractBtn = document.getElementById('subtract')

    // 绑定事件(View向Controller传达指令)
    addBtn.addEventListener('click', controller.handleAdd)
    subtractBtn.addEventListener('click', controller.handleSubtract)

    // 观察者模式响应接收数据变化
    this.model = model
    model.addObserve(this)
    this.renderView = function() {
      numDom.innerHTML = this.model.num
    }
  },

  Controller: function() {
    this.model = null
    this.view = null

    // 初始化
    this.init = function() {
      this.model = new App.Model()
      this.view = new App.View(this, this.model)
    }

    // 处理业务逻辑
    this.handleAdd = () => {
      const num = this.model.num + 1
      this.model.changeNum(num)
    }

    this.handleSubtract = () => {
      const num = this.model.num - 1
      this.model.changeNum(num)
    }
  }
}
const c = new App.Controller()
c.init()
MVP

MVP模式首先就是把Controller改名为Presenter

  • 模型(Model):数据保存
  • 视图(View):用户界面
  • 中间人(Presenter):业务逻辑

MVP模式相对于MVC最大的区别就是让Model层与View层解耦,所有的通信都通过Presenter 来完成。

image

从上面的代码中可以看到ViewModel之间采用了观察者模式响应。而对于MVC与MVP的区别,很容易想到这很类似观察者模式与发布订阅模式的区别,即通过一个中间人完成通信,通信的两端是不需要感知对方存在。

const App = {
  Model: function(presenter) {
    this.num = 0

    this.changeNum = function(num) {
      this.num = num
      // 发布
      presenter.emmit('numChange', num)
    }
  },

  View: function(presenter, model) {
    const numDom = document.getElementById('num')

    const addBtn = document.getElementById('add')
    const subtractBtn = document.getElementById('subtract')

    // 绑定事件(View向Presenter传达指令)
    addBtn.addEventListener('click', presenter.handleAdd)
    subtractBtn.addEventListener('click', presenter.handleSubtract)

    // 订阅
    presenter.on('numChange', function(num) {
      numDom.innerHTML = num
    })
  },

  // 依然由presenter处理业务逻辑的同时,还增加了负责View与Model之间的通信工作
  Presenter: function() {
    this.model = null
    this.view = null

    // 建立发布订阅模式调度中心
    this.list = {} // 消息队列

    // 订阅方法
    this.on = function(event, fn) {
      if(!this.list[event]) {
        this.list[event] = []
      }

      this.list[event].push(fn)
    }

    // 发布方法
    this.emmit = function(event, ...arg) {
      if(this.list[event]) {
        this.list[event].forEach(fn => {
          return fn.apply(this, arg)
        })
      }
    }

    // 初始化
    this.init = function() {
      this.model = new App.Model(this)
      this.view = new App.View(this, this.model)
    }

    // 处理业务逻辑
    this.handleAdd = () => {
      const num = this.model.num + 1
      this.model.changeNum(num)
    }

        // 处理业务逻辑
    this.handleSubtract = () => {
      const num = this.model.num - 1
      this.model.changeNum(num)
    }
  }
}
MVVM

MVP模式首先就是把Presenter改名为ViewModel

  • 模型(Model):数据保存
  • 视图(View):用户界面
  • 自动中间人(ViewModel):业务逻辑

它与MVP的区别就是通过双向绑定的方式,让开发者不用再去关心View与Model之间的通信。View的变动,自动反映在 ViewModel,ViewModel的变动,也自动反映到View。
image

比如在vue中,上面的例子可能会写类似这样的代码:

<div id="myapp">
  <div>
      <span>数字:{{ num }}</span>
  </div>
  <div>
      <button v-on:click="handleAdd">-</button>
      <button v-on:click="handleSubtract">+</button>
  </div>
</div>
{
  data() {
    return {
      num: 0
    }
  },

  methods: {
    handleAdd() {
      this.num = this.num + 1
    },
    handleSubtract() {
      this.num = this.num - 1
    }
  }
}

只要在ViewModel中改变了数据,视图就会自动更新。

总结

MV(X)模式的根本目的就是将程序的数据,界面以及业务逻辑解耦。区别主要在于(X)层承担的职责大小与实现方式的不同。MVP和MVVM都是基于MVC模式的分层衍生而来。

除了书中本章,其他查阅资料主要参考:

MVC,MVP 和 MVVM 的图示
浅析前端开发中的 MVC/MVP/MVVM 模式