🏖️ 认识 MVC
MVC的概念是从后端开发引入的,全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,是一种代码的设计模式。
Model:数据模型层,对数据进行增删改查的操作,操作数据库。View:视图层,显示视图或者视图模版。Controller:控制器层,该层主要将数据和视图进行关联挂载,和基本的逻辑操作。
它们 3 个的关系大概如下:
前端View发起请求 =>> Controller接收到请求 =>> 让Model去操作数据库 =>> 返回给Controller =>> 返回到前端
如果后端有
view层那这就是服务端渲染,由后端渲染完成后返回到前端。

🏖️ 前端中的 MVC
到了前端中,MVC的设计模式和后端基本一致:
Model管理视图需要的数据,数据和视图进行关联。View视图层,HTML模版和视图渲染。Controller管理事件的逻辑操作。
例如我们实现一个简单的MVC结构,实现一个加减乘除的案例。
我们先把整体的分层结构写出来:
(function () {function init() {model.init(); // 组织数据,监听数据(数据代理)view.render(); // 组织 HTML 模版 + 渲染 HTML 模版controller.init(); // 事件处理函数的定义和绑定}// 数据层var model = {};// 视图层var view = {};// 控制层var controller = {};init();})();
然后我们先把Model层的数据定义好:
(function () {function init() {model.init(); // 组织数据,监听数据(数据代理)view.render(); // 组织 HTML 模版 + 渲染 HTML 模版controller.init(); // 事件处理函数的定义和绑定}// 数据层var model = {// 数据管理data: {a: 0,b: 0,s: "+",r: 0,},// 对数据进行劫持init: function () {var _this = this;for (const key in _this.data) {Object.defineProperty(_this, key, {// 当使用 model.a 访问数据就被劫持get: function () {return _this.data[key];},set: function (newVal) {_this.data[key] = newVal;// 去进行更新视图的渲染view.render({[key]: newVal,});}});}}};// 视图层var view = {};// 控制层var controller = {};init();})();
以上代码,我们在Model层的data中定义了要被劫持的数据,在init方法中通过Object.defineProperty对数据的get和set进行相关处理,当set某个属性的时候,我们就会去更新视图层。
接下来我们去书写视图层的逻辑:
(function () {function init() {model.init(); // 组织数据,监听数据(数据代理)view.render(); // 组织 HTML 模版 + 渲染 HTML 模版controller.init(); // 事件处理函数的定义和绑定}// 数据层var model = {// 数据管理data: {a: 0,b: 0,s: "+",r: 0,},// 对数据进行劫持init: function () {var _this = this;for (const key in _this.data) {Object.defineProperty(_this, key, {// 当使用 model.a 访问数据就被劫持get: function () {return _this.data[key];},set: function (newVal) {_this.data[key] = newVal;// 去进行更新视图的渲染view.render({[key]: newVal,});}});}}};// 视图层var view = {el: "#app",template: `<div><span class="cla-a">{{ a }}</span><span class="cla-s">{{ s }}</span><span class="cla-b">{{ b }}</span><span>=</span><span class="cla-r">{{ r }}</span></div><div><input type="text" placholder="数字a" class="cal-input a" /><input type="text" placholder="数字b" class="cal-input b" /></div><div><button class="cla-btn">+</button><button class="cla-btn">-</button><button class="cla-btn">*</button><button class="cla-btn">/</button></div>`,render: function (mutedData) {// 处理数据更改// 首次在 init 执行的时候就会执行这里if (!mutedData) {// 利用正则表达式去匹配模版中的 {{ xxx }}// 然后把匹配到的 {{ xxx }} 替换为 Model 层中真实的数据this.template = this.template.replace(/\{\{(.*?)\}\}/g, function (node, key) {return model.data[key.trim()];});// 渲染到页面中var container = document.createElement("div");container.innerHTML = this.template;document.querySelector(this.el).appendChild(container);} else {// 遍历 Model 中的数据替换要更新的节点数据for (const key in mutedData) {document.querySelector(".cla-" + key).textContent = mutedData[key];}}}};// 控制层var controller = {};init();})();
以上代码,我们在view层的template中给标签都增加了一个类名cla-*这是为了我们方便后面去更新对应的数据。
在render方法中分别对第一次初始化和Model调用进行区分处理,渲染页面。
最后,就是我们的Controller层了:
(function () {function init() {model.init(); // 组织数据,监听数据(数据代理)view.render(); // 组织 HTML 模版 + 渲染 HTML 模版controller.init(); // 事件处理函数的定义和绑定}// 数据层var model = {// 数据管理data: {a: 0,b: 0,s: "+",r: 0},// 对数据进行劫持init: function () {var _this = this;for (const key in _this.data) {Object.defineProperty(_this, key, {// 当使用 model.a 访问数据就被劫持get: function () {return _this.data[key];},set: function (newVal) {_this.data[key] = newVal;// 去进行更新视图的渲染view.render({[key]: newVal,});}});}}};// 视图层var view = {el: "#app",template: `<div><span class="cla-a">{{ a }}</span><span class="cla-s">{{ s }}</span><span class="cla-b">{{ b }}</span><span>=</span><span class="cla-r">{{ r }}</span></div><div><input type="text" placholder="数字a" class="cal-input a" /><input type="text" placholder="数字b" class="cal-input b" /></div><div><button class="cla-btn">+</button><button class="cla-btn">-</button><button class="cla-btn">*</button><button class="cla-btn">/</button></div>`,render: function (mutedData) {// 处理数据更改// 首次在 init 执行的时候就会执行这里if (!mutedData) {// 利用正则表达式去匹配模版中的 {{ xxx }}// 然后把匹配到的 {{ xxx }} 替换为 Model 层中真实的数据this.template = this.template.replace(/\{\{(.*?)\}\}/g, function (node, key) {return model.data[key.trim()];});// 渲染到页面中var container = document.createElement("div");container.innerHTML = this.template;document.querySelector(this.el).appendChild(container);} else {// 遍历 Model 中的数据替换要更新的节点数据for (const key in mutedData) {document.querySelector(".cla-" + key).textContent = mutedData[key];}}}};// 控制层var controller = {init: function () {var oCalInputs = document.querySelectorAll(".cal-input"),oBtns = document.querySelectorAll(".cla-btn"),inputItem,btnItem;// 给所有的输入框框绑定 input 事件for (let index = 0; index < oCalInputs.length; index++) {inputItem = oCalInputs[index];inputItem.addEventListener("input", this.handleInput, false);}// 给所有的按钮绑定 click 事件for (let index = 0; index < oBtns.length; index++) {btnItem = oBtns[index];btnItem.addEventListener("click", this.handleClick, false);}},// 处理表单输入handleInput: function (event) {var tar = event.target,value = Number(tar.value),field = tar.className.split(" ")[1]; // 拿到输入框的 a 和 b// 然后去操作 model 中对应的值,然后就会触发 set 机制,然后就去渲染 dommodel[field] = value;// ES3 的写法,和 model.r = xxx 一个意思// 详见MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/withwith (model) {r = eval("a" + s + "b");}},handleClick: function (event) {var type = event.target.textContent;model.s = type;with (model) {r = eval("a" + s + "b");}}};init();})();
以上代码,我们在Controller层分别给输入框和按钮绑定了相关的事件,当事件触发的时候它们就会去操作Model的数据,然后就会触发属性的set机制,我们在set机制里面调用了View层的render方法,这个时候就产生数据模版,页面也就跟着变化。
🏖️ MVC 的缺点
通过上面的案例我们发现,这样的设计模式还不是特点的完美,View层本应该只关注于是数据的展示。但里面却包含了render方法,我们希望的是有一套驱动,能把数据、视图、事件处理都放在一起集中处理,这就是ViewModel。
MVC 是 MVVM 模型的雏形,MVVM 解决了驱动不内聚的缺点。
Model层管理数据data =>> 通过ViewModel层进行连接操作 =>> View层关注视图
这样开发者只关注于Model和View就可以啦,回到Vue框架中,Vue是关注于视图渲染的。
另外Vue允许通过ref来直接操作DOM,所以严格来说Vue并没有完全遵循MVVM的模型,MVVM是强制M和V完全分离的!
