MVC是一种设计模式,所有页面都可以使用MVC来优化代码结构,即每个模块可以写成三个对象 —— M, V, C
M是Model,数据模型,负责数据相关的任务
V是View,视图,负责所有UI界面
C是Controller,负责监听事件,调用M和V更新数据和视图
实现几个简单的模块
源码
模块化
模块化是指将一个复杂的系统划分为若干个模块,每个模块完成特定的功能,再将这些子模块合并在一起,形成一个整体,从而实现系统需要的功能。
对于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可以划分为如下图所示的结构
简略代码如下
// 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脚本修改数据后渲染即可
// 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()