MVC设计模式

678 阅读4分钟

设计模式就是对通用的代码起个名字。

  • 为什么要有设计模式? DRY Don't Repeat Yourself
    因为有 代码级别的重复、页面级别的重复,所以应该想出一个万金油的写法来优化代码结构。MVC就是一个万金油

「一」MVC三个对象

  • 把页面分成很多模块,每个模块可以写成3个对象,分别是M、V、C
  • M —— Model,数据模型,负责数据相关的任务,操作所有数据(增删改查)
  • V —— View,视图,负责所有UI界面,即用户能看得到的界面
  • C —— Controller,控制器,负责监听用户事件,然后调用 M 和 V 更新数据和视图

1. Model(数据模型)

Model 数据管理,包括数据逻辑、数据请求、数据存储等功能。
前端 Model 主要负责 AJAX 请求或者 LocalStorage 存储。

伪代码示例

const model = {
  data: { 初始化数据 },
  create() { 增数据 },
  delete() { 删数据 },
  update(data) { 更新数据 },
  get() { 读数据 }
}

2. View(视图)

View 负责用户界面,前端 View 主要负责 HTML 渲染。

伪代码示例

const view={
    // 需要刷新的元素
    el: null,
    // 显示在页面上的内容
    html: `<div class="html"></div>`// 初始化html
    init(container){
        v.el = $(container)       
    },
    // 刷新页面
    render() { 将数据渲染到页面 }
}

3. Controller(控制器)

Controller 负责处理 View 的事件,并更新 Model;也负责监听 Model 的变化,并更新 View,Controller 控制其他的所有流程。

伪代码示例

const controller = {
   init(){
      v.init() // view初始化
      v.render() // 第一次渲染 view = render(data)
      c.autoBindEvents() // 自动绑定事件
      eventBus.on('m:update', () => { v.render() }) // 当eventBus触发'm:update'时View刷新
   },
   events:{ 事件以哈希表方式记录 },
   method() {
      data = 改变后的新数据
      m.update(data)
   },
   autoBindEvents() { 自动绑定事件 }
}

4. MVC实例

一个加减乘除计算器
每次点击对应的按钮,数值就会变,基本思想就是监听click事件。

其MVC操作如下:

代码展示

import './app1.css'
import $ from 'jquery'

const eventBus = $(window)

//数据相关 都放到 M
const m = {
    data: {
        // 初始化数据n,从本地数据库拿到的数字n
        n: parseInt(localStorage.getItem('n'))
    },
    create(){},
    delete(){},
    update(data){
        Object.assign(m.data, data)
        eventBus.trigger('m:updated')
        localStorage.setItem('n', m.data.n)
    },
    get(){}
}

//视图相关 都放到 V
const v = {
    el: null,
    html: `
    <div>
    <div class="output">
        <span id="number">{{n}}</span>
    </div>
    <div>
        <button id="add1">+1</button>
        <button id="minus1">-1</button>
        <button id="mul2">×2</button>
        <button id="divide2">÷2</button>
    </div>
    </div>
    `,
    // 初始化html
    init(container){
        v.el = $(container)       
    },
    // 将数据渲染到页面
    render(n){
        if(v.el.children.length === null){ // 如果没有渲染过
        } else {
            v.el.empty()
        }
        $(v.html.replace('{{n}}', n)).appendTo(v.el)
    },
}

//其他 都 C
const c = {
    init(container){
        v.init(container)   // 第一次渲染html
        v.render(m.data.n)  // 第一次 view = render(data)
        c.autoBindEvents()   // 绑定事件
        eventBus.on('m:updated', ()=>{
            v.render(m.data.n)
        })
    },
    events: {
        'click #add1': 'add',
        'click #minus1': 'minus',
        'click #mul2': 'mul',
        'click #divide2': 'div'
    },
    autoBindEvents(){
        for(let key in c.events){
            const value = c[c.events[key]]
            const spaceIndex = key.indexOf(' ')
            const part1 = key.slice(0, spaceIndex)
            const part2 = key.slice(spaceIndex+1)
            v.el.on(part1, part2, value)
        }
    },
    add(){
        m.update({n: m.data.n + 1})
    },
    minus(){
        m.update({n: m.data.n - 1})
    },
    mul(){
        m.update({n: m.data.n * 2})
    },
    div(){
        m.update({n: m.data.n / 2})
    }
}

export default c

「二」 EventBus

  • EventBus 就是 EventTarget
  • EventBus也是一种设计模式或框架,主要用于组件/对象间通信的优化简化。
    • 比如在上面的例子中,Model 数据模型 和View 视图模型彼此不知道彼此的存在,但是又需要通信,于是就要用到EventBus
  • 优势 使用 eventBus 可以满足最小知识原则,m 和 v 互相不知道对方的细节,但是却可以互相调用对方的功能;代码简洁;速度快且轻量。

EventBus API

  • eventBus 提供了 onofftrigger 等 API,on 用于监听事件,trigger 用于触发事件,off 用于取消监听

代码实例

  • 在JS文件中自己声明一个EventBus,来模拟使用EventBus的功能。为了方便起见,这里通过引入jquery实现。
import $ from 'jquery'

const eventBus = $(window)
  • 在一个地方使用on方法,另外一个地方使用trigger方法,就初步实现了数据通信功能。
    • 在上面的MVC模型中, Model数据模型更新时,会 trigger 触发一个事件
    • 然后在controller会用 on 监听事件, 通知 view 模型去重新渲染页面
const m = {
  ....
  update(data) {
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')  // 通知一下view层,我已经更新了数据,view该开始工作了
    localStorage.setItem('n', m.data.n)
  },
  ....
}
const c = {
  init(container) {
    v.init(container)
    v.render(m.data.n) // view = render(data)
    c.autoBindEvents()
    eventBus.on('m:updated', () => {   // 用 on  监听事件, 通知 view 模型去重新渲染页面
      console.log('here')
      v.render(m.data.n)
    })
  },
  ... 
}

「三」表驱动编程

  • 表驱动法(Table-Driven Approach),简单讲是指用查表的方法获取值。
  • 表驱动法是一种编程模式,表里可以存数据,也可以存指令,或函数等都可以。
  • 在数值不多的时候我们可以用逻辑语句(if/else 或 case do)的方法来获取值,但随着数值的增多逻辑语句就会越来越长,此时我们可以用表驱动编程,把用来做If条件判断的值存进一个哈希表,然后从表里取值。
  • 表驱动编程在 JavaScript 中的一个重要应用是自动绑定事件。

代码展示

在上面的MVC实例中,加减乘除四个按钮我需要分别判断是哪一个按钮被点击,再修改output的值,
用事件委推,绑定事件中有jquery的事件监听方法。

--------用事件委托后-------
  const c = {
    init(container) {
        v.init(container)
        v.render(m.data.n)
        c.BindEvents()   //绑定事件
    }
    BindEvents() {
        //jquery对象
        v.el.on('click', '#add1', () => {
            m.data.n += 1
            v.render(m.data.n)
        })
        v.el.on('click', '#minus1', () => {
            m.data.n -= 1
            v.render(m.data.n)
        })
        v.el.on('click', '#mul2', () => {
            m.data.n *= 2
            v.render(m.data.n)
        })
        v.el.on('click', '#divide2', () => {
            m.data.n /= 2
            v.render(m.data.n)
        })
    }
}

但上面的代码,有很多重复的地方。用表驱动编程可以简化这些代码。把那些相同相似的地方“隐藏”,只留下需要的。

解决步骤:1. 绑定加减乘除按钮的父元素,只用一个事件监听 2.用哈希表存下按钮和按钮对应的操作

const c = {
    init() {
        ...
        // 称为自动绑定事件
        c.autoBindEvents()
    },
    // 事件 用 哈希表
    events: {
        'click #add1': 'add',
        'click #minus1': 'minus',
        'click #mul2': 'mul',
        'click #divide2': 'div'
    },
    autoBindEvents(){
        for(let key in c.events){
            const value = c[c.events[key]]
            const spaceIndex = key.indexOf(' ')
            const part1 = key.slice(0, spaceIndex)
            const part2 = key.slice(spaceIndex+1)
            v.el.on(part1, part2, value)
        }
    },
    add(){
        m.update({n: m.data.n + 1})
    },
    minus(){
        m.update({n: m.data.n - 1})
    },
    mul(){
        m.update({n: m.data.n * 2})
    },
    div(){
        m.update({n: m.data.n / 2})
    }
}

「四」模块化

  • 一个web页面,根据不同的功能可以分出不同的模块,每个模块的实现方式和使用的技术等等都不相同,引入模块化,可以切断每个模块的相互影响,使得单个模块中可以更好的优化代码。

  • 一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,如果外部想读取模块内部的某个变量,ES6的语法里引入了importexport用来实现。

  • 模块化可以降低代码耦合度,减少重复代码,提高代码重用性,并且在项目结构上更加清晰,便于维护。

References

  1. developer.mozilla.org/zh-CN/docs/…
  2. zhuanlan.zhihu.com/p/96985491