🏖️ 认识 MVC

MVC的概念是从后端开发引入的,全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,是一种代码的设计模式。

  • Model:数据模型层,对数据进行增删改查的操作,操作数据库。
  • View:视图层,显示视图或者视图模版。
  • Controller:控制器层,该层主要将数据和视图进行关联挂载,和基本的逻辑操作。

它们 3 个的关系大概如下:
前端View发起请求 =>> Controller接收到请求 =>> 让Model去操作数据库 =>> 返回给Controller =>> 返回到前端

如果后端有view层那这就是服务端渲染,由后端渲染完成后返回到前端。

image.png

🏖️ 前端中的 MVC

到了前端中,MVC的设计模式和后端基本一致:

  • Model管理视图需要的数据,数据和视图进行关联。
  • View视图层,HTML模版和视图渲染。
  • Controller管理事件的逻辑操作。

例如我们实现一个简单的MVC结构,实现一个加减乘除的案例。
我们先把整体的分层结构写出来:

  1. (function () {
  2. function init() {
  3. model.init(); // 组织数据,监听数据(数据代理)
  4. view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
  5. controller.init(); // 事件处理函数的定义和绑定
  6. }
  7. // 数据层
  8. var model = {
  9. };
  10. // 视图层
  11. var view = {
  12. };
  13. // 控制层
  14. var controller = {
  15. };
  16. init();
  17. })();

然后我们先把Model层的数据定义好:

  1. (function () {
  2. function init() {
  3. model.init(); // 组织数据,监听数据(数据代理)
  4. view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
  5. controller.init(); // 事件处理函数的定义和绑定
  6. }
  7. // 数据层
  8. var model = {
  9. // 数据管理
  10. data: {
  11. a: 0,
  12. b: 0,
  13. s: "+",
  14. r: 0,
  15. },
  16. // 对数据进行劫持
  17. init: function () {
  18. var _this = this;
  19. for (const key in _this.data) {
  20. Object.defineProperty(_this, key, {
  21. // 当使用 model.a 访问数据就被劫持
  22. get: function () {
  23. return _this.data[key];
  24. },
  25. set: function (newVal) {
  26. _this.data[key] = newVal;
  27. // 去进行更新视图的渲染
  28. view.render({
  29. [key]: newVal,
  30. });
  31. }
  32. });
  33. }
  34. }
  35. };
  36. // 视图层
  37. var view = {
  38. };
  39. // 控制层
  40. var controller = {
  41. };
  42. init();
  43. })();

以上代码,我们在Model层的data中定义了要被劫持的数据,在init方法中通过Object.defineProperty对数据的getset进行相关处理,当set某个属性的时候,我们就会去更新视图层。

接下来我们去书写视图层的逻辑:

  1. (function () {
  2. function init() {
  3. model.init(); // 组织数据,监听数据(数据代理)
  4. view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
  5. controller.init(); // 事件处理函数的定义和绑定
  6. }
  7. // 数据层
  8. var model = {
  9. // 数据管理
  10. data: {
  11. a: 0,
  12. b: 0,
  13. s: "+",
  14. r: 0,
  15. },
  16. // 对数据进行劫持
  17. init: function () {
  18. var _this = this;
  19. for (const key in _this.data) {
  20. Object.defineProperty(_this, key, {
  21. // 当使用 model.a 访问数据就被劫持
  22. get: function () {
  23. return _this.data[key];
  24. },
  25. set: function (newVal) {
  26. _this.data[key] = newVal;
  27. // 去进行更新视图的渲染
  28. view.render({
  29. [key]: newVal,
  30. });
  31. }
  32. });
  33. }
  34. }
  35. };
  36. // 视图层
  37. var view = {
  38. el: "#app",
  39. template: `
  40. <div>
  41. <span class="cla-a">{{ a }}</span>
  42. <span class="cla-s">{{ s }}</span>
  43. <span class="cla-b">{{ b }}</span>
  44. <span>=</span>
  45. <span class="cla-r">{{ r }}</span>
  46. </div>
  47. <div>
  48. <input type="text" placholder="数字a" class="cal-input a" />
  49. <input type="text" placholder="数字b" class="cal-input b" />
  50. </div>
  51. <div>
  52. <button class="cla-btn">+</button>
  53. <button class="cla-btn">-</button>
  54. <button class="cla-btn">*</button>
  55. <button class="cla-btn">/</button>
  56. </div>
  57. `,
  58. render: function (mutedData) {
  59. // 处理数据更改
  60. // 首次在 init 执行的时候就会执行这里
  61. if (!mutedData) {
  62. // 利用正则表达式去匹配模版中的 {{ xxx }}
  63. // 然后把匹配到的 {{ xxx }} 替换为 Model 层中真实的数据
  64. this.template = this.template.replace(/\{\{(.*?)\}\}/g, function (node, key) {
  65. return model.data[key.trim()];
  66. });
  67. // 渲染到页面中
  68. var container = document.createElement("div");
  69. container.innerHTML = this.template;
  70. document.querySelector(this.el).appendChild(container);
  71. } else {
  72. // 遍历 Model 中的数据替换要更新的节点数据
  73. for (const key in mutedData) {
  74. document.querySelector(".cla-" + key).textContent = mutedData[key];
  75. }
  76. }
  77. }
  78. };
  79. // 控制层
  80. var controller = {};
  81. init();
  82. })();

以上代码,我们在view层的template中给标签都增加了一个类名cla-*这是为了我们方便后面去更新对应的数据。
render方法中分别对第一次初始化和Model调用进行区分处理,渲染页面。

最后,就是我们的Controller层了:

  1. (function () {
  2. function init() {
  3. model.init(); // 组织数据,监听数据(数据代理)
  4. view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
  5. controller.init(); // 事件处理函数的定义和绑定
  6. }
  7. // 数据层
  8. var model = {
  9. // 数据管理
  10. data: {
  11. a: 0,
  12. b: 0,
  13. s: "+",
  14. r: 0
  15. },
  16. // 对数据进行劫持
  17. init: function () {
  18. var _this = this;
  19. for (const key in _this.data) {
  20. Object.defineProperty(_this, key, {
  21. // 当使用 model.a 访问数据就被劫持
  22. get: function () {
  23. return _this.data[key];
  24. },
  25. set: function (newVal) {
  26. _this.data[key] = newVal;
  27. // 去进行更新视图的渲染
  28. view.render({
  29. [key]: newVal,
  30. });
  31. }
  32. });
  33. }
  34. }
  35. };
  36. // 视图层
  37. var view = {
  38. el: "#app",
  39. template: `
  40. <div>
  41. <span class="cla-a">{{ a }}</span>
  42. <span class="cla-s">{{ s }}</span>
  43. <span class="cla-b">{{ b }}</span>
  44. <span>=</span>
  45. <span class="cla-r">{{ r }}</span>
  46. </div>
  47. <div>
  48. <input type="text" placholder="数字a" class="cal-input a" />
  49. <input type="text" placholder="数字b" class="cal-input b" />
  50. </div>
  51. <div>
  52. <button class="cla-btn">+</button>
  53. <button class="cla-btn">-</button>
  54. <button class="cla-btn">*</button>
  55. <button class="cla-btn">/</button>
  56. </div>
  57. `,
  58. render: function (mutedData) {
  59. // 处理数据更改
  60. // 首次在 init 执行的时候就会执行这里
  61. if (!mutedData) {
  62. // 利用正则表达式去匹配模版中的 {{ xxx }}
  63. // 然后把匹配到的 {{ xxx }} 替换为 Model 层中真实的数据
  64. this.template = this.template.replace(/\{\{(.*?)\}\}/g, function (node, key) {
  65. return model.data[key.trim()];
  66. });
  67. // 渲染到页面中
  68. var container = document.createElement("div");
  69. container.innerHTML = this.template;
  70. document.querySelector(this.el).appendChild(container);
  71. } else {
  72. // 遍历 Model 中的数据替换要更新的节点数据
  73. for (const key in mutedData) {
  74. document.querySelector(".cla-" + key).textContent = mutedData[key];
  75. }
  76. }
  77. }
  78. };
  79. // 控制层
  80. var controller = {
  81. init: function () {
  82. var oCalInputs = document.querySelectorAll(".cal-input"),
  83. oBtns = document.querySelectorAll(".cla-btn"),
  84. inputItem,
  85. btnItem;
  86. // 给所有的输入框框绑定 input 事件
  87. for (let index = 0; index < oCalInputs.length; index++) {
  88. inputItem = oCalInputs[index];
  89. inputItem.addEventListener("input", this.handleInput, false);
  90. }
  91. // 给所有的按钮绑定 click 事件
  92. for (let index = 0; index < oBtns.length; index++) {
  93. btnItem = oBtns[index];
  94. btnItem.addEventListener("click", this.handleClick, false);
  95. }
  96. },
  97. // 处理表单输入
  98. handleInput: function (event) {
  99. var tar = event.target,
  100. value = Number(tar.value),
  101. field = tar.className.split(" ")[1]; // 拿到输入框的 a 和 b
  102. // 然后去操作 model 中对应的值,然后就会触发 set 机制,然后就去渲染 dom
  103. model[field] = value;
  104. // ES3 的写法,和 model.r = xxx 一个意思
  105. // 详见MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with
  106. with (model) {
  107. r = eval("a" + s + "b");
  108. }
  109. },
  110. handleClick: function (event) {
  111. var type = event.target.textContent;
  112. model.s = type;
  113. with (model) {
  114. r = eval("a" + s + "b");
  115. }
  116. }
  117. };
  118. init();
  119. })();

以上代码,我们在Controller层分别给输入框和按钮绑定了相关的事件,当事件触发的时候它们就会去操作Model的数据,然后就会触发属性的set机制,我们在set机制里面调用了View层的render方法,这个时候就产生数据模版,页面也就跟着变化。
屏幕录制2023-02-07 10.25.06.gif

🏖️ MVC 的缺点

通过上面的案例我们发现,这样的设计模式还不是特点的完美,View层本应该只关注于是数据的展示。但里面却包含了render方法,我们希望的是有一套驱动,能把数据、视图、事件处理都放在一起集中处理,这就是ViewModel

MVC 是 MVVM 模型的雏形,MVVM 解决了驱动不内聚的缺点。

Model层管理数据data =>> 通过ViewModel层进行连接操作 =>> View层关注视图

这样开发者只关注于ModelView就可以啦,回到Vue框架中,Vue是关注于视图渲染的。
另外Vue允许通过ref来直接操作DOM,所以严格来说Vue并没有完全遵循MVVM的模型,MVVM是强制MV完全分离的!