浅谈MVC

179 阅读9分钟

MVC是一种设计模式,设计模式通俗的讲就是一些优秀的程序员认为自己写的代码很优秀,认为别人也会用到,于是把代码做了做通用化,进而对这个通用的代码起个名字,这就是设计模式;那为什么要有设计模式呢?因为是DRY原则,don't repeat yourself;那么在代码界什么样的代码算重复呢?其实我们有明确的界定:

  • 代码级别的重复
    • 比如把相同的三行代码写两遍,这是不行的,这是不可容忍的,因为你一旦写了两遍就有可能写三遍,写了三遍就有可能写四遍,如果后面需求变动,你就会要找到所有的重复的代码的地方去修改,这是让人非常担心的事情,有可能你忘记修改了,bug出现的几率就提高了;那么你应该重构它,把相同的代码使用方法包装起来,用到的时候调用就完事;写代码没有任何复杂的技巧,编程是崇尚简单优雅的职业,如果你做的东西不够简单,那你一定是做错了,
  • 页面级重复
    • 如果你把类似的页面做了多次,那你应该要想到一个万金油的写法,再做页面的时候就不需要思考,直接简单地做出来;如果你已经做了多遍,那你就应该能发现其中的规律,如果你发现不了,你就相当于一个机器,让你做什么就做什么;
  • 那有没有一个设计模式,是所有前端都可以用的设计模式,这就是MVC,MVC就是一个万金油,所有的页面都可以使用MVC来优化你的代码结构;

如果我们不学MVC会怎么样呢

  • 首先你就找不到工作了;
  • 从代码层次上讲,如果你不学MVC,你就会写出意大利面条式的代码;老手程序员为了鄙视烂代码,称其代码为面条式代码;
  • 如果你不学那些流行又好用的设计模式,你将变成外包式程序员;
    • 不停重复自己,不懂地抽象
    • 只会调用API,不懂得提升自己
    • 只会写业务,不会封装,更不会造轮子

MVC到底是啥

  • 每个模块都可以分成三个对象,分别是M、V、C
  • M-Model(数据模型)负责操作所有数据
  • V-View(视图)负责所有UI界面
  • C-Controller(控制器)负责其他 MVC没有明确的定义,每个程序员对MVC的定义是稍微有些不同,意会,大概对就可以;

下面通过一个加减乘除的例子来讲解MVC

  • 1.安装插件jquery,使用import引入文件,代替之前在页面直接引入的方式;之前我们引入jquery是从页面直接引用,现在我们使用npm或者yarn方式安装,先使用yarn,yarn用不了就用jquery

  • 2.安装完jquery包之后如何使用呢,index.js里面引进jquery

  • 3.没有使用MVC之前的代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<!--计算器-->
<div id="app1">
  <p id="numberText"></p>
  <div class="btns">
    <button > +1 </button>
    <button > -1 </button>
    <button > *2 </button>
    <button > /2 </button>
  </div>
</div>
<!--其他效果-->
<div id="app2">

</div>
<div id="app3"></div>
<div id="app4"></div>
<script src="./index.js"></script>

</body>
</html>
import './app1.css'
import './reset.css'
import $ from 'jquery'
const $numberText = $('#numberText');
const n = localStorage.getItem('n')
$numberText.text(n || 0);
const [$addBtn,$minitBtn,$multiBtn,$dividBtn] = $('.btns button');
console.log($addBtn);
$($addBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n++;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($minitBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n--;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($multiBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n*=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($dividBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n/=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})

4. 使用最小知识原则,最开始引入一个模块需要引入html、js、css,我们进一步抽象化后如上例,引入一个模块只需要html、js(css在js中引进了),最后我们再进一步抽象,引入一个模块只需要js(我们可以在js中引入html、css);代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<!--计算器-->
<!--其他效果-->
<div id="app2">

</div>
<div id="app3"></div>
<div id="app4"></div>
<script src="./index.js"></script>

</body>
</html>

import './app1.css'
import './reset.css'
import $ from 'jquery'
const html = `
<div id="app1">
  <p id="numberText"></p>
  <div class="btns">
    <button > +1 </button>
    <button > -1 </button>
    <button > *2 </button>
    <button > /2 </button>
  </div>
</div>
`
// 把html填到body里面
const $html = $(html);
$html.prependTo(document.body);
const $numberText = $('#numberText');
const n = localStorage.getItem('n')
$numberText.text(n || 0);
const [$addBtn,$minitBtn,$multiBtn,$dividBtn] = $('.btns button');
$($addBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n++;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($minitBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n--;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($multiBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n*=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($dividBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n/=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})

不过这种方式是有代价的,这样做会使页面一开始是空白、没内容没样式; 219.gif

解决办法:加loading菊花、加骨架、加占位内容等 5.以不变应万变(设置mvc三个对象),我们先理清代码的功能,请看注释

import './app1.css'
import './reset.css'
import $ from 'jquery'
// 初始化html
const html = `
<div id="app1">
  <p id="numberText"></p>
  <div class="btns">
    <button > +1 </button>
    <button > -1 </button>
    <button > *2 </button>
    <button > /2 </button>
  </div>
</div>
`
const $html = $(html);
$html.prependTo(document.body);

// 选择元素
const $numberText = $('#numberText');
const [$addBtn,$minitBtn,$multiBtn,$dividBtn] = $('.btns button');

// 初始化数据
const n = localStorage.getItem('n')

// 将数据渲染到页面
$numberText.text(n || 0);

// 绑定事件
$($addBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n++;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($minitBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n--;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($multiBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n*=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($dividBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n/=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})

6.我们可以把数据相关的放到m对象,视图相关的代码放入v对象,其他代码放入c对象,结构改造后如下:先把v对象改造

import './app1.css'
import './reset.css'
import $ from 'jquery'
// 数据代码放入m对象
// 其他代码放入c对象
// 视图代码放入v对象
let v = {
  html : `
<div id="app1">
  <p id="numberText"></p>
  <div class="btns">
    <button > +1 </button>
    <button > -1 </button>
    <button > *2 </button>
    <button > /2 </button>
  </div>
</div>
`,
  render(){
    const $html = $(v.html);
    $html.prependTo(document.body);
  }
}
// 第一次渲染
v.render();
// 选择元素
const $numberText = $('#numberText');
const [$addBtn,$minitBtn,$multiBtn,$dividBtn] = $('.btns button');

// 初始化数据
const n = localStorage.getItem('n')

// 将数据渲染到页面
$numberText.text(n || 0);

// 绑定事件
$($addBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n++;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($minitBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n--;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($multiBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n*=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})
$($dividBtn).on('click',()=>{
  let n = parseInt($numberText.text());
  n/=2;
  localStorage.setItem('n',n);
  $numberText.text(n)
})

以下是再次优化后的代码

import './app1.css'
import $ from 'jquery'
// 数据代码放入m对象
const m = {
  // 初始化数据
  data:{
    n : localStorage.getItem('n')
  }

}
// 视图代码放入v对象
const v = {
  el:null,
  html : `
  <p id="numberText">{{n}}</p>
  <div class="btns">
    <button id="btn1"> +1 </button>
    <button id="btn2"> -1 </button>
    <button id="btn3"> *2 </button>
    <button id="btn4"> /2 </button>
  </div>
`,
  init(container){
    v.container = container;
    v.render(container);
  },
  render(container){
    if(v.el === null) {
      v.el = $(v.html.replace('{{n}}',m.data.n)).prependTo(container);
    } else {
      const newEl = $(v.html.replace('{{n}}',m.data.n));
       v.el.replaceWith(newEl);
       v.el = newEl;
    }
  }
}

// 其他代码放入c对象
const c = {
  // 选择元素
  init(container){
    v.init(container);
    c.ui = {
      numberText: $('#numberText'),
      addBtn: $('#btn1'),
      minitBtn: $('#btn2'),
      multiBtn: $('#btn3'),
      dividBtn: $('#btn4'),
    }
    c.bindEvents()
  },
  bindEvents(){
    $(v.container).on('click','#btn1',()=>{
      m.data.n ++
      localStorage.setItem('n',m.data.n);
      v.render(v.container);
    })
    $(v.container).on('click','#btn2',()=>{
      m.data.n --
      localStorage.setItem('n',m.data.n);
      v.render(v.container);
    })
    $(v.container).on('click','#btn3',()=>{
      m.data.n *=2
      localStorage.setItem('n',m.data.n);
      v.render(v.container);
    })
    $(v.container).on('click','#btn4',()=>{
      m.data.n /=2
      localStorage.setItem('n',m.data.n);
      v.render(v.container);
    })
  }
}
export default  c;

7.其实上面的代码我们还未发现mvc的好处,因为我们还缺少些过程,还处于一个比较初级的阶段,所以我们需要进一步改进与抽象;
首先,我们可以去掉查找元素的代码

c.ui = {
      numberText: $('#numberText'),
      addBtn: $('#btn1'),
      minitBtn: $('#btn2'),
      multiBtn: $('#btn3'),
      dividBtn: $('#btn4'),
    }

这些查找的元素在整个代码中不会用到了,所以可以删除;视图对象v里面的el与container可以合并成一个,改造后的代码如下:

const v = {
  container:null,
  html : `
  <p id="numberText">{{n}}</p>
  <div class="btns">
    <button id="btn1"> +1 </button>
    <button id="btn2"> -1 </button>
    <button id="btn3"> *2 </button>
    <button id="btn4"> /2 </button>
  </div>
`,
  init(container){
    v.container = $(container);
    v.render();
  },
  render(){
    if(v.container.children.length !== 0) v.container.empty();
    $(v.html.replace('{{n}}',m.data.n)).prependTo(v.container);
  }
}

由于container名字太长了,我们可以把名字改成el,最后优化的代码如下,以下代码任何的地方都不能删除了,删除了就会缺失,就会报错;

import './app1.css'
import $ from 'jquery'
// 数据代码放入m对象
const m = {
  // 初始化数据
  data:{
    n : localStorage.getItem('n')
  }

}
// 视图代码放入v对象
const v = {
  el:null,
  html : `
  <p id="numberText">{{n}}</p>
  <div class="btns">
    <button id="btn1"> +1 </button>
    <button id="btn2"> -1 </button>
    <button id="btn3"> *2 </button>
    <button id="btn4"> /2 </button>
  </div>
`,
  init(el){
    v.el = $(el);
    v.render();
  },
  render(){
    if(v.el.children.length !== 0) v.el.empty();
    $(v.html.replace('{{n}}',m.data.n)).prependTo(v.el);
  }
}

// 其他代码放入c对象
const c = {
  // 选择元素
  init(el){
    v.init(el);
    c.bindEvents()
  },
  bindEvents(){
    $(v.el).on('click','#btn1',()=>{
      m.data.n ++
      localStorage.setItem('n',m.data.n);
      v.render(v.el);
    })
    $(v.el).on('click','#btn2',()=>{
      m.data.n --
      localStorage.setItem('n',m.data.n);
      v.render(v.el);
    })
    $(v.el).on('click','#btn3',()=>{
      m.data.n *=2
      localStorage.setItem('n',m.data.n);
      v.render(v.el);
    })
    $(v.el).on('click','#btn4',()=>{
      m.data.n /=2
      localStorage.setItem('n',m.data.n);
      v.render(v.el);
    })
  }
}
export default  c;

那我们再思考下,能不能把以上代码再简化一下,因为我们只看代码实在看不懂这些代码在干什么,所以我们要引进以下思想,

所有的视图就是把数据渲染一下,view = render(data),我们写页面实际上是在js写代码操作的是dom,以下图是我们使用mvc的操作过程与数据流向

MVC认为这种操作非常的麻烦,因为不管更新什么,我每次都要做这个操作,

8.根据以上思想,我们可以再对代码进行简化,绑定事件的部分进行简化,把相同的部分隐藏掉,只留我们需要的部分;

import './app1.css'
import $ from 'jquery'
// 使用eventBus现实自定义事件的触发与监听,以下eventBus有个trigger函数可以触发自定义事件,on可以监听自定义事件
const eventBus = $({})
// 数据代码放入m对象
const m = {
  // 初始化数据
  data:{
    n : localStorage.getItem('n')
  },
  update(data){
    localStorage.setItem('n',data.n )
    Object.assign(m.data,data);
    eventBus.trigger('update')
  }

}
// 视图代码放入v对象
const v = {
  el:null,
  html : `
  <p id="numberText">{{n}}</p>
  <div class="btns">
    <button id="btn1"> +1 </button>
    <button id="btn2"> -1 </button>
    <button id="btn3"> *2 </button>
    <button id="btn4"> /2 </button>
  </div>
`,
  init(el){
    v.el = $(el);
    v.render();
  },
  render(){
    if(v.el.children.length !== 0) v.el.empty();
    $(v.html.replace('{{n}}',m.data.n)).prependTo(v.el);
  }
}

// 其他代码放入c对象
const c = {
  // 选择元素
  init(el){
    v.init(el);
    c.autoBindEvents();
    eventBus.on('update',()=>{
      v.render()
    })
  },
  events: {
    'click #btn1': 'add',
    'click #btn2': 'minus',
    'click #btn3': 'mul',
    'click #btn4': 'div',
  },
  add(){
    m.update({n: parseInt(m.data.n) + 1})
  },
  minus(){
    m.update({n: parseInt(m.data.n) - 1})
  },
  mul(){
    m.update({n: parseInt(m.data.n) * 2})
  },
  div(){
    m.update({n:  parseInt(m.data.n) / 2})
  },
  autoBindEvents(){
    for(let key in c.events) {
      let keyArr = key.split(' ');
      let eventName = keyArr[0];
      let eleName = keyArr[1];
      let funName = c.events[key]
      v.el.on(eventName,eleName,()=>{
        c[funName]();
      })
    }
  }
}
export default  c;

9.上面的代码使用了自动绑定事件思想与eventBus对象间的通讯思想;

10.编程:事不过三原则;同样的代码写三遍,就应该抽成一个函数;同样的属性写三遍,就应该做成共用属性(原型或者类);同样的原型写三遍,就应该使用继承; 代价就是:有时候会造成继承层级太深,无法一下看懂代码;可以通过写文档、画类图来解决; app1.js代码中,m对象中,data对象都不一样,create、delete、get、update是一样,我们可以使用类进行抽离;新建一个文件夹,命名为base,在里面放一个Model.js文件;Model.js的代码如下:告诉别人所有的Model都有create、delete、get、update属性

class Model {
  create(){
  }
  delete(){
  }
  get(){
  }
  update(){
  }
}

如果想要传data到Model类里面,需要使用constructor

class Model {
  constructor(data){
    // data为此对象的data
    this.data = data;
  }
  // 下面的方法都为原型链上的方法
  create(){
  }
  delete(){
  }
  get(){
  }
  update(){
  }
}
export default Model;

在app1.js更改如下

import Model from "./base/Model";
const m = new Model({n : localStorage.getItem('n')});
m.update = (data)=>{
  localStorage.setItem('n',data.n )
  Object.assign(m.data,data);
  eventBus.trigger('update')
}

由于c对象的代码比较少,而且两者也有关联,所以我们可以把c与v进行合并, vue是直接把m也省络了,mvc全部合并成了一个Vue对象;

总结

1.mvc思想是以不变应万变,每个模块都可以使用m+v+c搞定,每个模块这样写就可以了,不用再思考类似的需求该怎么做;但是这样做也是有代价的,有时候需求简单,其实用mvc还复杂,会多出很多代码;有时候遇到特殊情况不知道怎么变通,比如没有html的模块怎么做mvc; 2.mvc思想的使用必然会用到表驱动编程,也就是数据驱动编程,就是我们上面例子中的自动绑定事件的实现过程就是表驱动编程思想;使用场景:当你看到大批类似但不重复的代码,眯起眼睛看看到底哪些才是重要的数据,把重要的数据做成哈希表,你的代码就简单了,这是数据结构给我们的红利; 3.view = render(data)思想,此思想早就了react;比起操作dom,直接render要简单很多,只要改变data,就能得到对应的view;但是这种方式也是有代价的,render的粗犷的渲染肯定比DOM操作浪费性能;还好用到了虚拟DOM,虚拟DOM让render只更新该更新的地方;react是使用的此方式渲染页面,vue不是,vue是局部准备更新变化的部分; 4.事不过三原则;同样的代码写三遍,就应该抽成一个函数;同样的属性写三遍,就应该做成共用属性(原型或者类);同样的原型写三遍,就应该使用继承;