MVC
模型基础理论
MVC
英文MVC``Model-View-Controller
是软件设计中的一种模式,通常用于实现用户界面、数据和控制逻辑。它强调了软件的业务分离逻辑和显示之间的分离。这种“关注点分离”提供了更好的劳动分工和更好的维护。其它一些设计模式是基于MVC
的,比如MVVM
、MVW
等
MVC
软件设计模式的三个部分可以描述如下:
Model
:管理数据和业务逻辑。View
:处理布局与视图模板。Controller
:处理数据和视图关联挂载和基本的逻辑操作。
前端中的MVC
前端中的MVC
分为:
Model
:管理视图所需要的数据、数据的关联。View
:HTML
模板与视图渲染。Controller
:管理事件逻辑。
MVC
模式实现计算器
我们现在通过MVC
模式来实现一个计算器的功能。既然MVC
层中牵扯到Model
、View
、Controller
层,那么我们就要搞清楚这三个层中到底控制了些什么?
首先是Model
层:Model
层管理视图所需要的数据,对于计算器来说需要用到哪些数据data
呢?首先肯定包含两个需要计算的数字,我们先称为number1、number2
。肯定还存在计算的方式,既然是计算那么肯定有加减乘除的方式,我们先称为type
。最后肯定还要保存计算的结果res
。
其次是View
层:View
层管理视图和视图渲染,我们要在视图层中构建HTML
模板。
最后是Controller
层:控制器contoller
层包含绑定的事件处理函数,因为当我们点击计算的按钮时,视图要能够随时计算出更新后的结果。
计算器整个流程中其实包含双向绑定的过程:
- 首先是
controller
触发事件更新Model
层数据,Model
层数据更新后通过View
层更新渲染。 - 其次是
View
层手动input
更新数据,View
层更新通知controller
层控制模型层Model
中的数据更新。
MVC
模式实现计算器的步骤
- 搭建
MVC
模式需要的架子,首先是Model、View、Controller
模型层。我需要把所有的视图都放到根元素#app
容器内部,所以要在视图中手动创建#app
根元素。
// 视图
<div id="app"></div>
// 基本MVC逻辑的架子
;(function(){
function init(){
model.init(); // 组织数据 + 数据监听操作 / 数据代理
view.render();// 组织HTML模版+渲染HTML模版
controller.init(); // 事件处理函数定义与绑定
};
init();
})()
- 开始编写
Model
层的逻辑,Model
模型层处理的是什么问题呢?Model
其实处理就是数据劫持、数据响应的问题。- 首先我们要在
Model
模型层保存计算器功能需要用到的数据a、b、s、r
。 - 其次我们要用数据劫持的逻辑处理下一步问题。为什么需要用到数据劫持呢?因为我们现在想访问到
Model
中的data
数据的话,我们需要通过Model.data.a
的方式进行访问。而我需要的是直接通过Model.a
的方式进行访问。其次是因为当Model
模型层中的数据发生改变的时候,View
层视图也会随着更新。换句话说,就是当我们操作了Model
模型层中的数据时,我们不仅仅需要更新Model
层中的数据,还需要操作视图的更新。所以要用到数据劫持的方式。 - 在枚举
_this.data
对象的时候,为什么我们要用立即执行函数呢?如果不添加立即执行函数的话,当我们在触发getter、setter
函数的时候,getter、setter
函数内部访问到的key
都是循环之后的结果。通过立即执行函数,让getter、setter
函数能够在自己的词法作用域内部存在私有变量key
。换句话说,就是getter、setter
函数是闭包函数,通过立即执行函数创建一个独立的作用域,闭包函数能够访问到独立作用域中的私有变量key
。
- 首先我们要在
var model = {
data:{
a:0, // input1
b:0, // input2
s:'+', // 方法
r:0 // 结果
},
init:function(){
// 保存this指向
var _this = this;
// 枚举_this.data属性进行数据劫持
for (const k in _this.data) {
if (Object.hasOwnProperty.call(_this.data, k)) {
(function(k){
Object.defineProperty(_this,k,{
get:function(){
// model.a -> get
return _this.data[k];
},
set:function(newValue){
// model.a = 123 - set
// 更新Model模型层的数据
_this.data[k] = newValue;
// view.update({[k]:newValue});
}
})
})(k)
}
}
}
};
- 开始编写
View
层中的逻辑,View
模型层中需要处理的逻辑是什么呢?首先肯定是视图模板、视图渲染、视图更新的逻辑。- 首先编写模板视图
View
视图层目前的逻辑分为两部分,首先是视图初始化渲染,什么是视图初始化渲染呢?也就是视图第一次渲染,将所有的视图都进行渲染显示。其次就是视图更新渲染,当Model
数据发生变化时,我们要部分渲染视图。所以View
视图层暂时就分为render、update
两种方法。render
函数渲染的逻辑思路:首先我们需要将View
模板中的{{ a }}
内容进行替换,我们要把花括号内部的内容全部替换成与Model
模型层数据对应的值。所以我们通过正则表达式,将{{}}
内容取出,然后用Model
模型层中的数据进行替换。替换之后,我们要将替换后的模板内容当作#app
容器的内容。update
函数渲染的逻辑思路:首先update
函数会接受一个需要更新的对象,对象内部的键值对表示更新的Model
数据和更新后的值。所以我们可以枚举这个更新的对象,然后通过document.querySelector
查找出需要更新的DOM
元素,将该DOM
元素修改为更新后的值即可。为什么要通过类名的方式寻找DOM
元素呢?因为我们没有进行收集依赖的操作,我们只有通过类名的方式找到需要更新的DOM
元素。- 到目前为止,我们实现了
MVC
模式中Model
层与View
层的关系,Model
层中的数据发生变化时,View
层就会更新视图。
var view = {
el:'#app',
template:`
<p>
<span class="cal-a">{{a}}</span>
<span class="cal-s">{{s}}</span>
<span class="cal-b">{{b}}</span>
<span>=</span>
<span class="cal-r">{{r}}</span>
</p>
<p>
<input type="text" placeholder="Number a" class="cal-input a" />
<input type="text" placeholder="Number b" class="cal-input b" />
</p>
<p>
<button class="cal-btn">+</button>
<button class="cal-btn">-</button>
<button class="cal-btn">*</button>
<button class="cal-btn">/</button>
</p>
`,
render:function(){
this.template = this.template.replace(/\{\{(.*?)\}\}/g,function(node,key){
return model[key]
})
var container = document.createElement('div');
container.innerHTML = this.template;
document.querySelector(this.el).appendChild(container);
},
update:function(mutedData){
if(!mutedData) return;
for (const k in mutedData) {
if (Object.hasOwnProperty.call(mutedData, k)) {
document.querySelector('.cal-'+k).textContent = mutedData[k]
}
}
}
};
- 开始编写
Controller
层的逻辑。Controller
模型层的逻辑是什么呢?Controller
模型层肯定是事件处理函数的绑定、事件处理的逻辑。- 首先我们需要处理
input
输入框的逻辑,当输入框输入值的时候,视图要随之发生改变。编写input
输入框事件处理函数的逻辑也比较简单,首先我们需要通过querySelectorAll
操作所有的input
元素节点,然后依次绑定给其绑定事件处理函数。然后我们需要获取当前事件源对象输入的value
值,然后将value
赋值给Model
模型层中对应的数据。最后再重新计算当前Model
中res
结果。
- 首先我们需要处理
- 其次我们处理计算按钮点击的逻辑,当点击按钮时,需要更新计算的结果,并且视图也要随着发生改变。按钮的事件处理函数绑定,我用的是事件委托的方式进行处理。其实逻辑与
input
元素绑定事件处理函数相同,都是给Model
模型层对应的数据赋值之后,再重新计算Model
模型层中res
的值。 - 注意在计算
Model
数据层中的值时,用到了with
语句和eval()
函数,with
语句主要目的是修改当前作用域链的顶层作用域链。而eval
语句是为了能够通过字符串的形式计算Model
数据层中的res
数据结果。
var controller = {
init:function(){
var oCalInputs = document.querySelectorAll('.cal-input'),
oCalBtns = document.querySelectorAll('.cal-btn'),
btnItem,
inputItem;
for (let i = 0; i < oCalInputs.length; i++) {
inputItem = oCalInputs[i];
inputItem.addEventListener('input',this.handleInput,false);
};
for (let i = 0; i < oCalBtns.length; i++) {
btnItem = oCalBtns[i];
btnItem.addEventListener('click',this.handleBtnClick,false);
}
},
handleInput:function(e){
var tar = e.target,
value = Number(tar.value),
field = tar.className.split(' ')[1];
model[field] = value;
// model.r = eval('model.a'+model.s+'model.b')
with(model){
r = eval('a'+s+'b')
}
},
handleBtnClick:function(e){
var type = e.target.textContent;
model.s = type;
with(model){
r = eval('a'+s+'b')
}
}
}
到目前为止,我们MVC
模式的计算器功能已经完全实现。仔细想一想,MVC
模型中的每个模型层分别注重的是什么?
Model
模型层:保存计算器中用到的数据、主要处理数据劫持、数据保存功能。
View
模型层:处理计算器的视图渲染、主要处理模板渲染、模板更新功能。
Controller
模型层:处理输入框事件处理函数绑定、按钮事件处理函数绑定、主要处理事件、处理事件处理函数的绑定。
我们发现MVC
模型并不是为了解决DOM
操作的问题,而是将每个模型层进行分离。每个模型层关注点不同。而我们之前说过MVC
模型中的双向绑定,在计算器开发的过程中也体现出来:
计算器整个流程中其实包含双向绑定的过程:
- 首先是
controller
触发事件更新Model
层数据,Model
层数据更新后通过View
层更新渲染 - 其次是
View
层手动input
更新数据,View
层更新通知controller
层控制模型层Model
中的数据更新。