浅析MVC

72 阅读3分钟

img

MVC是一种设计模式,所有页面都可以使用MVC来优化代码结构,即每个模块可以写成三个对象 —— M, V, C

M是Model,数据模型,负责数据相关的任务

V是View,视图,负责所有UI界面

C是Controller,负责监听事件,调用M和V更新数据和视图

实现几个简单的模块

img

源码

github.com/AmberWANGDM…

模块化

模块化是指将一个复杂的系统划分为若干个模块,每个模块完成特定的功能,再将这些子模块合并在一起,形成一个整体,从而实现系统需要的功能。

对于JS的模块化,主要解决的问题是

  • 全局变量污染
  • 命名冲突
  • 繁琐的文件依赖

前两个问题可以通过闭包配合立即执行函数来解决,而解决第三个问题就用到了CommonJS(Nodejs)、AMD、CMD和UMD这些社区认可的统一模块化规范,以及官方模块化规范ES Module。

ESM模块化功能主要由两个命令构成:exports和import,export命令规定模块的对外接口,import命令用于输入其他模块的功能。ES6还提供了export default的命令,为模块指定默认输出,对应的import语句不需要大括号。

详细解释参考developer.mozilla.org/zh-CN/docs/…

实现

在main.js开头引入各个模块

 import './reset.css'
 import './global.css'
 ​
 import c from './app1'
 import './app2'
 import './app3'
 import './app4'

在app1.js中引入css文件,默认输出

 import $ from 'jquery'
 import './app1.css'
 ​
 export default c

ccs文件中也可以引入css文件,使用@import(src),但性能较差

MVC优化代码

这个模块的功能是实现加减乘除,按照M V C可以划分为如下图所示的结构

img

简略代码如下

 // app1.js
 // ============================ Model ===============================
 const m = {
   data:{}
 }
 ​
 // ============================ View ==================================
 const v = {
   // 容器 由外部传入
   el:null,
   // html
   html:``,
   // 初始化渲染
   init(el){
     v.render()
   },
   // 渲染
   render(){
     // if (容器不为空) 清空后再appendTo(v.html)以更新
   }
 }
 ​
 // ========================== Controller ==============================
 const c = {
   init(el){
     v.init(el)
     c.bindEvents()
   },
   bindEvents(){
     v.el.on(事件,监听对象,()=>{/*修改数据,重新渲染页面*/}) // 事件绑定在不变元素
   }
 }
 ​
 export default c
 ​
 ​
 // main.js
 import c from './app1.js'
 c.init('css选择器') //传入容器el

view = render(data) 思维

在使用原生DOM操作修改数据渲染页面时,JS先获取数据,再修改DOM;而使用 view = render(data)时,只需在JS脚本修改数据后渲染即可

img

 // app1.js
 // ============================ View ==================================
 const v = {
   el:null,
   html:``,
   init(el){}, // 用jQuery封装el
   render(n){}
 }
 ​
 // ========================== Controller ==============================
 const c = {
   init(el){
     v.init(el)
     v.render(data) // view = render(data)
     c.bindEvents()
   },
   bindEvents(){
     v.el.on(事件,监听元素,()=>{
       v.render(new data) // view = render(data)
     }) 
   }
 }

表驱动编程思维

将不同的部分抽象成一个对象(哈希表)进行遍历,对于重复度比较高的代码,例如

 const $button1 = $('.add')
 const $button2 = $('.sub')
 const $button3 = $('.mul')
 const $button4 = $('.div')
 const $number = $('#resultNum')
 ​
 // 绑定鼠标事件
 $button1.on('click', () => {
   let n = parseInt($number.text())
   n += 1
   localStorage.setItem('res', n)
   $number.text(n)
 })
 $button2.on('click', () => {
   let n = parseInt($number.text())
   n -= 1
   localStorage.setItem('res', n)
   $number.text(n)
 })
 $button3.on('click', () => {
   let n = parseInt($number.text())
   n *= 2
   localStorage.setItem('res', n)
   $number.text(n)
 })
 $button4.on('click', () => {
   let n = parseInt($number.text())
   n /= 2
   localStorage.setItem('res', n)
   $number.text(n)
 })

修改

 // ========================== Controller ==============================
 const c = {
   init(container){
     v.init(el)
     v.render(data)
     c.autoBindEvents()
   },
   events:{
     'click .add':'add',
     'click .sub':'sub',
     'click .mul':'mul',
     'click .div':'div'
   },
   add(){
     data += 1
     v.render(data)
   },
   sub(){
     data -= 1
     v.render(data)
   },
   mul(){
     data *= 2
     v.render(data)
   },
   div(){
     data /= 2
     v.render(data)
   }
   ,
   autoBindEvents(){
     // 遍历哈希表绑定事件
     for(let key in c.events){
       const event = key.split(' ')[0]
       const element = key.split(' ')[1]
       v.el.on(event,element,c[c.events[key]])
     }
   }
 }

EventBus

MVC三层之间需要通信时,这时就需要用到EventBus(事件总线)。EventBus主要用于组件之间的监听与通信。

API

EventBus.on()监听事件

EventBus.trigger()触发事件

EventBus.on()取消监听事件

修改代码,实现只要『数据改变就自动渲染页面』的功能,在Vue中watch也是类似的实现

 // 点击按钮,调用Model的update函数更新数据
 // update函数中eventbus对象会利用trigger触发更新事件m:update,
 // Controller中eventbus对象利用on监听到数据更新事件,就重新渲染页面,不需要每个按钮的触发事件里都写一遍render(data)了
 const eventBus = $(window) // api - on, trigger
 ​
 // ============================ Model ===============================
 const m = {
   data:{},
   update(data){
     Object.assign(m.data,data)
     eventBus.trigger('m:update') // 2 触发事件
   },
 }
 ​
 // ========================== Controller ==============================
 const c = {
   init(el){
     eventBus.on('m:update',()=>{ // 3 监听事件
       v.render(data)
     })
   },
   add(){
     m.update(data) // 1 修改数据
   }
 }

类和继承

同样的代码写三遍,就应该抽成一个函数;同样的属性写三遍,就应该做成共用属性(原型或类);同样的原型写三遍,就应该用继承

代价:有的时候会造成继承层级太深,无法一下看懂代码。可以通过写文档、画类图解决

使用类进一步优化代码

Model

data是独享的属性,增删改查操作是共有的

 // 类 Model.js
 class Model {
   constructor(options) {
     const keys = ['data', 'update', 'create', 'delete', 'get']
     keys.forEach((key) => {
         if (key in options) {
             this[key] = options[key]
         }
     })
   }
   // 共有属性
   create() {}
   delete() {}
   update() {}
   get() {}
 }
 ​
 export default Model
 ​
 ​
 // app1.js
 import Model from "./base/Model";
 ​
 const m = new Model({
    data:{},
   update:()=>{}
 })

View 和 Contoller 合并

因为View和Controller很难完全分离,必须通过Controller拿到el(容器),然后才能初始化View,将两者合并

 // 类 View.js
 import $ from 'jquery'
 ​
 class View {
   constructor(options) { 
     Object.assign(this,options)
     this.el = $(el)
     this.render(this.data)
     this.autoBindEvents()
     this.eventBus.on('m:updated',()=>{this.render(this.data))
    },
   autoBindEvents() {}
 }
 ​
 export default View
 ​
 ​
 // app1.js
 import View from "./base/View"
 ​
 // 合并 c 和 v
 const init = (el) => {
     new View({
         el: el,
         data: m.data,
         html: ``,
         eventBus: eventBus,
         render(data) {},
         events: {},
     })
 }
 export default init

EventBus

继承EventBus:让Model和View的实例都可以直接on trigger

EventBus一般都放在祖先类,以DOM元素为例,倒数第二层类,或者说原型链倒数第二个,是EventTarget(最后一个是根对象Object),这使得所有DOM元素都可以触发和监听事件

 import $ from 'jquery'
 ​
 class EventBus {
     constructor() {
         this._eventBus = $(window)
     }
 ​
     on(eventName, fn) {
         return this._eventBus.on(eventName, fn)
     }
 ​
     trigger(eventName, data) {
         return this._eventBus.trigger(eventName, data)
     }
 ​
     off(eventName, fn) {
         return this._eventBus.off(eventName, fn)
     }
 }
 ​
 export default EventBus
 ​
 // Model和View用extends继承
 // app1.js
 const eventBus = new EventBus()

源码

github.com/AmberWANGDM…