什么是MVC?
- M :model, 即数据模型,负责数据相关的任务,包括对数据的增删改查
- V :view, 即视图层,即用户能看得到的界面
- C :Controller,控制器,负责监听用户事件,然后调用 M 和 V 更新数据和视图
关于MVC,没有一个明确的定义,每个人心目中都有不同的MVC,下面根据我的理解,给出MVC的伪代码示例。
Model 数据模型
const m ={
data:{数据源},
create:{增加数据},
delete:{删除数据},
update(data){
Object.assign(m.data,data)//用新数据替换旧数据
eventBus.trigger('m:update')//eventBus触发'm:update'信息,通知View刷新界面
},
get:{获取数据}
}
View 视图层
const v ={
el:要刷新的元素,
html:'要显示在页面上的刷新内容'
init(){
v.el:初始化需要刷新的元素
},
render(){
重新渲染页面
}
}
Controller 控制器
控制器通过绑定时间,根据用户的操作,调用M与V更新视图与数据
假设我们现在需要做一个简单的加减乘除器
就可以书写这样的控制器来实现控制
const c ={
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})
},
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)
}
}
EventBus
EventBus主要是用于对象之间通信,MVC的三层是互相独立的,它们不知道彼此的存在,所以当它们之间需要通信时,就要用到EventBus。
EventBus主要API
- on : 用于监听事件
- trigger :用于触发事件
- off : 用于取消监听
如下:
const m = {
...
update(data) {
...
eventBus.trigger('m:updated')
...
},
...
}
我们在m层中触发事件,更新数据
const c = {
init(container) {
...
eventBus.on('m:updated', () => {
v.render(m.data.n)
})
},
...
}
c层就会通过on监听到这个事件,并通知v层去把页面重新渲染
表驱动编程
当我们需要判断多种情况来执行事件时,往往需要很多的if…else,这样会使代码看上去很不优雅,且如果判断中相同的语句太多,更是会显得代码臃肿,所以我们可以构造一个哈希表,把多个条件存入中,从而提升代码的可读性,这也就是表驱动编程
如上方的加减乘除例子,如果我们使用普通的方法,
$button1.on("click", () => {
let n = parseFloat($number.text());
n += 1;
localStorage.setItem("n", n);
$number.text(n);
});
$button2.on("click", () => {
let n = parseFloat($number.text());
n -= 1;
localStorage.setItem("n", n);
$number.text(n);
});
$button3.on("click", () => {
let n = parseFloat($number.text());
n *= 2;
localStorage.setItem("n", n);
$number.text(n);
});
$button4.on("click", () => {
let n = parseFloat($number.text());
n /= 2;
localStorage.setItem("n", n);
$number.text(n);
});
这样的代码,重复度极高,可读性不强
于是,我们使用表驱动编程
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) { // 遍历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)
}
}
用这种方法,绑定按钮的父元素,用哈希表存下按钮与按钮对应的操作,之后再把表遍历来实现功能,大大增加代码的可读性。虽然在这个例子中似乎代码更加复杂了,但是事件越多它的简洁性就会越显著。
模块化
模块化是MVC的重要前提,把相对独立的代码从一大段代码里抽取成一个个短小的模块,每个模块相对独立,方便以后的更新和维护,这就是模块化。
ES6中,引入了import与export来实现模块化
export default c // 默认导出
export {c} // 另外一种导出方式。记得要加花括号
在另一个模块中,将其引入
import x from './app1.js'
{default as x} from './app1.js'//另一种引入方法
具体详见-->JavaScript modules 模块 - JavaScript | MDN (mozilla.org)
划分模块的一个准则是"高内聚低耦合"
从模块粒度来看,
-
高内聚:尽可能类的每个成员方法只完成一件事(最大限度的聚合);
-
低耦合:减少类内部,一个成员方法调用另一个成员方法。
从类角度来看,
- 高内聚低耦合:减少类内部,对其他类的调用;
从功能块来看
- 高内聚低耦合:减少模块之间的交互复杂度(接口数量,参数数据)
总结
MVC的抽象思维
1.最小知识原则
- 你需要知道的知识越少越好
- 模块化为这一点奠定了基础
2.以不变应万变
- 既然每个模块都可以用m+v+c搞定
- 那么每个模块都这样写就好了
- 不用再考虑类似的需求该怎么做了
3.表驱动编程
- 当有大批代码类似带不重复
- 把重要的数据做成哈希表,这样可以大大简化代码
4.事不过三
- 同样的代码写三遍,就应该抽象成一个函数
- 同样的属性写三遍,就应该做成共有属性(原型或类)
- 同样的原型写三遍,就应该用继承
5.俯瞰全局
把所有对象看成点
- 一个点与一个点怎么通信
- 一个点与多个点怎么通信
- 多个点与多个点怎么通信
- 最终我们找出一个专用的点负责通信
- 这个点就是EventBus
6.view = render(data)
- 只要改变data,就可以得到对应的view
- vue中并没有体现这个思想,但React中有体现