阅读 1194

「MVC」浅谈MVC模式

一、MVC 是什么?

M 就是 model,即数据模型,负责数据相关的任务,包括对数据的增删改查。
V 就是 view,即视图层,即用户能看得到的界面。
C 就是 Controller,即控制器,负责监听用户事件,然后调用 M 和 V 更新数据和视图。

最近了解了 MVC 设计模式,发现除了上述定义之外,MVC 并没有一个比较明确的做法,MVC 其实就是将代码变的结构化的一种抽象概念。

1.1 Model 数据模型

//示例
let Model = {
  data: { 数据源 },
  create: { 增加数据 },
  delete: { 删除数据 },
  update(data) {
    Object.assign(m.data, data); //用新数据替换旧数据
    eventBus.trigger("m:update"); //eventBus触发'm:update'信息,通知View刷新界面
  },
  get: { 获取数据 },
};
复制代码

1.2 View 视图层

//示例
let View={
    el:要刷新的元素,
    html:'要显示在页面上的刷新内容'
    init(){
        v.el:初始化需要刷新的元素
    },
    render(){
        刷新页面
    }
}
复制代码

1.3 Controller 控制器

控制器就是通过绑定事件,根据用户的操作,调用 M 和 V 更新数据和视图

let Controller={
    init(){
        v.init()//初始化View
        v.render()//第一次渲染页面
        c.autoBindEvents()//自动的事件绑定
        eventBus.on('m:update',()=>{v.render()}//当enentsBus触发'm:update'是View刷新
    },
    events:{事件以哈希表的方式记录存储},
    //例如:
   events: {
    'click #add1': 'add',
    'click #minus1': 'minus',
    'click #mul2': 'mul',
    'click #divide2': 'div',
    },
    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})
    },
    method(){
        data=新数据
        m.update(data) // controller 通知 model去更新数据
    },
    autoBindEvents(){
    	for (let key in c.events) { // 遍历events表,然后自动绑定事件
      const value = c[c.events[key]]
      const spaceIndex = key.indexOf(' ')
      const part1 = key.slice(0, spaceIndex) // 拿到 'click'
      const part2 = key.slice(spaceIndex + 1)  // 拿到'#add1'
      v.el.on(part1, part2, value)
    }
}
复制代码

1.4 MVC 实例

目标:做一个加减乘除计算器 1.gif

封装成 mvc 对象初版app1.js文件如下:

import "./app1.css";
import $ from "jquery";

// 初始化html
const html = ` 
<section id="app1">
<div class="result">
  <span id="number"></span>
</div>
<div class="actions">
  <button id="add">+1</button>
  <button id="minus">-1</button>
  <button id="mul">*2</button>
  <button id="divide">/2</button>
</div>`;
const $element = $(html).appendTo($("body"));

// 初始化数据
const $number = $("#number");
let number = parseInt(localStorage.getItem("number"));

// 将数据渲染到页面
$number.text(number || 100);
let n = parseInt($number.text());

// 找到需要操作的元素
const $add = $("#add");
const $minus = $("#minus");
const $mul = $("#mul");
const $divide = $("#divide");

// 绑定鼠标事件
$add.on("click", () => {
  n += 1;
  $number.text(n);
  localStorage.setItem("number", n);
});
$minus.on("click", () => {
  n -= 1;
  $number.text(n);
  localStorage.setItem("number", n);
});
$mul.on("click", () => {
  n *= 2;
  $number.text(n);
  localStorage.setItem("number", n);
});
$divide.on("click", () => {
  n /= 2;
  $number.text(n);
  localStorage.setItem("number", n);
});
复制代码

但是整个代码并没有那么结构化,我们可以把所有跟数据有关的都放到 m 对象里,视图相关的都放到 v 里,其他都放到 c 里,思路是初始化 n => 渲染界面 => 更新 n => 再次渲染界面

app1.js代码如下:

import "./app1.css";
import $ from "jquery";
// m数据
const m = {
  data: {
    n: parseInt(localStorage.getItem("number")),
  },
};
// v视图
const v = {
  el: null,
  html: ` 
  <div>
<div class="result">
  <span id="number">{{n}}</span>
</div>
<div class="actions">
  <button id="add">+1</button>
  <button id="minus">-1</button>
  <button id="mul">*2</button>
  <button id="divide">/2</button>
</div>
</div>`,
  init(el) {
    v.el = $(el);
    v.render();
  },
  // view = render(data)
  render(n) {
    if (v.el.children.length !== 0) v.el.empty();
    $(v.html.replace("{{n}}", n)).appendTo(v.el);
  },
};
// c控制器
const c = {
  init(el) {
    v.init(el);
    v.render(m.data.n);
    c.bindEvents();
  },
  // 绑定鼠标事件 事件委托
  bindEvents() {
    v.el.on("click", "#add", () => {
      m.data.n += 1;
      localStorage.setItem("number", m.data.n);
      v.render(m.data.n);
    });
    v.el.on("click", "#minus", () => {
      m.data.n -= 1;
      localStorage.setItem("number", m.data.n);
      v.render(m.data.n);
    });
    v.el.on("click", "#mul", () => {
      m.data.n *= 2;
      localStorage.setItem("number", m.data.n);
      v.render(m.data.n);
    });
    v.el.on("click", "#divide", () => {
      m.data.n /= 2;
      localStorage.setItem("number", m.data.n);
      v.render(m.data.n);
    });
  },
};
export default c;
复制代码

二、EventBus

2.1 EventBus 是什么?

EventBus 主要用于对象之间的通信,比如在上面的例子中,Model 数据模型 和 View 视图模型彼此不知道彼此的存在,但是又需要通信,于是就要用到 EventBus
总结:使用 eventBus 可以满足最小知识原则,m 和 v 互相不知道对方的细节,但是却可以调用对方的功能

2.2 EventBus 有哪些 API?

eventBus 提供了 on、off 和 trigger 等 API,on 用于监听事件,trigger 用于触发事件
比如在上面的 MVC 模型中, M 数据模型更新时,会 trigger 触发一个事件

const m = {
  ....
  update(data) {
    Object.assign(m.data, data)
    eventBus.trigger('m:updated')  // 通知一下view层,我已经更新了数据,view该开始工作了
    localStorage.setItem('n', m.data.n)
  },
  ....
}
复制代码

然后在 controller,controller 会用 on 监听事件, 然后通知 view 模型去重新渲染页面

const c = {
  init(container) {
    v.init(container)
    v.render(m.data.n) // view = render(data)
    c.autoBindEvents()
    eventBus.on('m:updated', () => {   // controller会用 on  监听事件,
      //然后通知 view 模型去重新渲染页面
      console.log('here')
      v.render(m.data.n)
    })
  },
  ...
}
复制代码

三、表驱动编程

当我们需要判断 3 种以上的情况,做出相应的事情,往往需要写很多很多的 If else,这样的代码可读性不强, 为了增强代码的可读性,我们可以用表驱动编程,把用来做 If 条件判断的值存进一个哈希表,然后从表里取值
举例:
在上面的例子中,加减乘除四个按钮我需要分别判断是哪一个按钮被点击,再修改 output 的值,
按照传统做法, 我们会对四个按钮分别绑定 click 事件,然后再分别写四个回调函数,修改值

$button1.on('click', () => {
    let n = parseInt($number.text())
    n += 1
    localStorage.setItem('n', n)
    $number.text(n)
})

$button2.on('click', () => {
    let n = parseInt($number.text())
    n -= 1
    localStorage.setItem('n', n)
    $number.text(n)
})

$button3.on('click', () => {
    let n = parseInt($number.text())
    n = n * 2
    localStorage.setItem('n', n)
    $number.text(n)
})

$button4.on('click', () => {
    let n = parseInt($number.text())
    n = n/2
    localStorage.setItem('n', n)
    $number.text(n)
})

--------用事件委托后-------
  const c = {
    init(container) {
        v.init(container)
        v.render(m.data.n)
        c.BindEvents()
    }
    BindEvents() {
        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 = {
  events: {
    'click #add1': 'add',
    'click #minus1': 'minus',
    'click #mul2': 'mul',
    'click #divide2': 'div',
  },
  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})
  },
  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)
    }
  }
复制代码

四、模块化

模块化就是把相对独立的代码从一大段代码里抽取成一个个短小精悍的模块
每个模块之间相对独立,方便以后的维护和修改
ES6 的语法里引入了 Import 和 export 就是用来实现模块化的
当我们在 app1.js 里封装好了 controller 模型, 然后导出 controller:

export default c; // 默认导出
export { c }; // 另外一种导出方式。记得要加花括号
复制代码

在 Main.js 里我们想用 controller:

import x from './app1.js'
等价于import {default as x} from './app1.js'

x.init('#app1')
复制代码

关于重命名导出的更多例子:

// inside module.mjs
export { function1, function2 };

// inside main.mjs
import {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName,
} from "/modules/module.mjs";
复制代码

五、总结

5.1 最小知识原则

  • 以最少的知识使用一个模块,平时我们需要引入 html,并在 html 里引入 css 和 js,现在只需要引入一个模块 js 就够了

  • 当有多个模块的时候。通过 main.js 作为入口文件引入各个模块的 js,并且不担心各个模块的 html 和 css,因为 js 会各自顾好自己。 有个问题:在网速比较慢的时候,需等待加载 js 完毕后才显示内容,出现白屏,可以添加菊花、骨架等提高用户体验

    5.2 m+v+c

  • 从这个演变过程来看,虽然最终 mvc 在 vue 里就浓缩成了一个 v,但是其实 vue 还是有 m(data)、v(template)、c(methods)这个思想在的,每个模块都可以按照这个模式来。

    5.3 抽象重复的东西

  • 同样的代码重复:抽象成函数

  • 同样的属性重复:抽象成原型或类

  • 同样的原型重复:使用继承

文章分类
前端
文章标签