MVC 与 Vue

本文写于 2020 年 7 月 27 日

首先有个问题:Vue 是 MVC 还是 MVVM 框架?

维基百科告诉我们:MVVM 是 PM 的变种,而 PM 又是 MVC 的变种。

所以一定程度上来说,不管 Vue 是 MVC 还是 MVVM 或者都不是,它的思想方向与这些设计模式的方向是大体相同的。

并且 Vue 的官网中也说道:“虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。”

这个问题网上吵得比较多,本文并不是来讨论这个问题的,而是面是向初学者浅浅的分析一下老大哥 MVC 的思想在 Vue 中的体现

0 新手的困惑

大学时候专业里前后开了几门网页课,先是教授 HTML、CCC;后来一门课教了 JS;最后有一门教授 Vue 的课。

由于我大学读的并不是计算机专业,而是艺术类的数字媒体艺术专业。所以大家对于编程的热情度几乎是负的。

上学期的 JS 都没学好,一听说要学 Vue,大家的内心自然是崩溃的。课程上来就是一段代码:

let app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});

大家一开始的心声就是这样的:什么?!这是什么?谁看得懂!

并且不光是初学者,一些写了一段时间 Vue 的人,懂得 el 是是什么、data 是什么,但可能也不清楚为什么 Vue 要这么来组织代码——除非他学过 MVC

1 一个 MVC 计数器

一个 MVC 模块是三个对象的合体:M, V, C。

  • M,即为 Model,代表数据;
  • V,即为 View,代表视图;
  • C,即为 Controller,代表控制(业务逻辑)。

严格来说……MCV 没有严格来说,MVC 的定义并不明确,所以我以为 MVC 其实是一种思想方向,代表着视图和业务逻辑互不干扰。

还是那句话,放码过来。我们先实现一个非常常见的例子:加按钮与减按钮。

普通版本 JS 计数器

<div id="app">
<span>0</span>
<button id="add">+</button>
<button id="minus">-</button>
</div>

我们希望的结果是,当我们点击 + 号时,<span> 中的数字就会 +1,点击 - 号时,同理就会 -1。

我相信这种 JS 代码应该是信手拈来的对吧。

const numberWrapper = document.querySelector('#app span');
const addBtn = document.querySelector('#add');
const minusBtn = document.querySelector('#minus'); addBtn.addEventListener('click', () => {
const newNumber = parseInt(numberWrapper.innerText) + 1;
numberWrapper.innerText = newNumber.toString();
}); minusBtn.addEventListener('click', () => {
const newNumber = parseInt(numberWrapper.innerText) - 1;
numberWrapper.innerText = newNumber.toString();
});

但这只是普通版,接下来让我们用 MVC 的方式来一步步的重构这个代码。

MVC 版本 JS 计数器

首先我们想,这样写的一个计数器,如果需要修改,那我一方面要改 HTML 文件、一方面还要修改 JS 文件,何其麻烦!

写到一起来吧:

const app = document.querySelector('#app');

const html = `
<span>0</span>
<button id="add">+</button>
<button id="minus">-</button>
`;
const counter = document.createElement('div');
counter.innerHTML = html;
app.appendChild(counter);

那么我们来梳理一下现在的代码:

  1. 首先我们需要创建 HTML 元素;
  2. 然后通过 CSS 选择器找到对应的 DOM 元素;
  3. 再对他们添加各种监听事件与操作。

那我们可以大胆的猜测一下嘛,如何使用 MVC 思想呢?

首先新建一个对象叫做 view 吧,再将我们的 html 代码放进去:

const view = {
html: `
<span>0</span>
<button id="add">+</button>
<button id="minus">-</button>
`
};

还有我们用来新建 div、将 html 代码放入 div、再将 div 放进 app 的操作,应该也是属于视图层。

所以我们给 view 对象添加一个 render 方法:

const view = {
// ...html...
render() {
const counter = document.createElement('div');
counter.innerHTML = view.html;
app.appendChild(counter);
}
}; view.render();

这样我们就搞定了 V,然后看看 C。除了视图和数据,其他的东西应该都属于 C,所以 DOM 元素的获取放在 C 里、事件绑定也放在 C 里。

const controller = {
ui: {},
bindEvents() {}
};

这里我们准备将 DOM 元素放在 ui 对象里,但是这里需要脑子转一下。

一旦我们在这里写了 querySelector,那么必然是找不到元素的,因为我们还没有 render,根本没有那些按钮和数字。

所以我们得在里面写一个 init 函数,这样我们执行初始化之后,他就会先去获取 DOM、再去绑定事件:

init() {
this.ui = {
numberWrapper: document.querySelector('#app span'),
addBtn: document.querySelector('#add'),
minusBtn: document.querySelector('#minus')
};
controller.bindEvents();
},

绑定事件的写法就非常简单了:

bindEvents() {
controller.ui.addBtn.addEventListener('click', () => {
const newNumber = parseInt(controller.ui.numberWrapper.innerText) + 1;
controller.ui.numberWrapper.innerText = newNumber.toString();
});
controller.ui.minusBtn.addEventListener('click', () => {
const newNumber = parseInt(controller.ui.numberWrapper.innerText) - 1;
controller.ui.numberWrapper.innerText = newNumber.toString();
});
}

接下来就是一个转折点了,我们要创建一个 model 对象来保存数据

const model = {
data: {
number: 100
}
};

这个时候不知道大家有没有领悟到一些东西。

既然已经有了 model,我们何必还去操作 DOM 获取数据呢?

直接操作 model 多优雅呀!

所以 bindEvents 可以改成这样:

controller.ui.addBtn.addEventListener('click', () => {
model.data.number += 1;
});
controller.ui.minusBtn.addEventListener('click', () => {
model.data.number -= 1;
});

那我们的 view 对象也需要修改,他也应该从 model 中获取数据:

const view = {
html: `
<span>{{number}}</span>
......
`,
render() {
const counter = document.createElement('div');
counter.innerHTML = view.html.replace('{{number}}', model.data.number);
app.appendChild(counter);
}
};

但是我们这样操作虽然说修改了数据,可是并没有重新渲染到页面上呀。所以每次提交之后需要重新 render。

此时问题出现了:点击 + 或者 - 后,数字只会变化一次,第二次点击便毫无用处!

这是为什么呢?

很简单,因为我们重新 render,导致俩绑定了事件的 button 全都不是曾经的那个他了

所以我们使用事件代理来解决这个问题——将事件绑定在外层的 div 上,然后判断点击对象的 id 即可。

写法如下:

const compute = e => {
switch (e.target.id) {
case 'add':
model.data.number += 1;
break;
case 'minus':
model.data.number -= 1;
break;
default:
return;
}
view.render();
};

接下来我们会在 view 对象中添加一个 el 属性,用来存储我们创建的外层 div。

const view = {
el: null,
// ......
render() {
if (!view.el) {
// 创建 div,并将 div 赋值给 el
} else {
// 将 el 的 innerHTML 更换为新的内容
}
}
};

最后我们再进行一步优化。

我们本身不应该知道在 render 时,应该 append 给哪一个元素。这个元素应该是别人传给我的,所以应该这么写:

总代码:

MVC 之 V

const view = {
el: null,
html: `
<span>{{n}}</span>
<button id="add">+</button>
<button id="minus">-</button>
`,
render(container) {
if (!view.el) {
const counter = document.createElement('div');
view.el = counter;
counter.innerHTML = view.html.replace(
'{{n}}',
model.data.number.toString()
);
container.appendChild(counter);
} else {
view.el.innerHTML = view.html.replace(
'{{n}}',
model.data.number.toString()
);
}
}
};

MVC 之 M

const model = {
data: {
number: parseInt(window.localStorage.getItem('number')) || 0
},
save() {
window.localStorage.setItem('number', model.data.number.toString());
}
};

MVC 之 C

const controller = {
init(container) {
controller.ui = {
container
};
view.render(container);
controller.bindEvents();
},
bindEvents() {
controller.ui.container.addEventListener('click', e => {
switch (e.target.id) {
case 'add':
model.data.number += 1;
break;
case 'minus':
model.data.number -= 1;
break;
default:
return;
}
model.save();
view.render();
});
}
};

使用方式:

const app = document.querySelector('#app');

controller.init(app);

这个时候我们的程序已经是一个比较完整的 MVC 模式了,但直接全部 render 非常浪费性能。

所以 React 之类的框架会使用虚拟 DOM 和 diff 算法来只修改变化的 DOM。

总的来说,我们的 MVC 思想可以抽想成为一个公式:view = render(data)

使用 class 来优化代码

class 优化代码可以提升我们的代码复用程度,铭记:程序员永远不要重复自己的操作

先看看 Model:

class Model {
constructor(options) {
for (let key in options) {
this[key] = options[key];
}
} save() {
console.error('还未传入save函数');
}
} export default Model;

这个非常简单,我们想要传入任何的东西,都在这个 option 里面,就像这样:

const model = new Model({
data: {},
save() {}
});

回想一下,我们使用 Vue 的时候,是不是也是如此?

export default new Vue({
data() {
return {
msg: 'hello world'
};
},
methods: {}
});

我没读过 Vue 的源码,不知道 Vue 是否是按照本文的思路构建代码的。

但是 Vue、React 等框架追根溯源都能找到 MVC 的身上。所以毫无疑问,MVC 的思想是每一个程序员都需要学习的一种设计模式。

初学程序,用了几个好用的框架与工具,不应该只沉迷于其方便的一面,要善于从工具的运用中寻找出其作者留下的蛛丝马迹,反推学习、多查资料,才能够慢慢进化成为不惧怕新技术、框架越来越多的大神程序员!

工具也许会一个月一变、一天一变,但是思维是永恒的。

(完)

MVC 与 Vue的更多相关文章

  1. 基于TeamCity的asp.net mvc/core,Vue 持续集成与自动部署

    一 Web Server(Windows)端的配置 1.配置IIS,重要的是管理服务 1.1 配置FTP(前端NPM项目需要) 该步骤略,如果是在阿里云ESC上,需要开启端口21(用来FTP认证握手) ...

  2. MVC框架+vue+elementUI

    用自动化构建做的vue项目,因为是动态加载数据,在SEO优化时一直不如意,于是我们换了框架,用MVC框架,做成静态页面,但是原来的代码都是用vue和elementUI,为了快速的复用原来的代码,于是在 ...

  3. .net MVC +EF+VUE做回合制游戏(一)

    刚毕业的新人,工作的时候试过用.net 框架,但是我发现写的前端代码都非常多,要写很多很多的原生,然后最近在看vue.js觉得还不错,可以减少前端很多dom操作. 至于做的东西我是想做一个游戏,一个回 ...

  4. .net MVC +EF+VUE做回合制游戏(二)

    Emmm,游戏中的属性购买页面 话不多说先上代码 <form id="vue" action="/ltgdGame.Web/Main/Index" met ...

  5. ASP.NET MVC+Vue.js实现联系人管理

    接触了一天vue.js,简单浏览了一本关于vue的电子书,就开始动手使用ASP.NET MVC和Vue.js开发一个联系人管理的小程序. 先看一下这个联系人管理的小程序的界面,也就是我们大概要实现什么 ...

  6. Java基础知识(壹)

    写在前面的话 这篇博客,是很早之前自己的学习Java基础知识的,所记录的内容,仅仅是当时学习的一个总结随笔.现在分享出来,希望能帮助大家,如有不足的,希望大家支出. 后续会继续分享基础知识手记.希望能 ...

  7. Web前端 web的学习之路

    零基础学习web前端的顺序 ( 转载自:https://blog.csdn.net/weixin_41780944/article/details/83751632) 怎么开始学习两条路:自学或者找培 ...

  8. 基于 HTML5 WebGL 构建智能数字化城市 3D 全景

    前言 自 2011 年我国城镇化率首次突破 50% 以来,<新型城镇化发展规划>将智慧城市列为我国城市发展的三大目标之一,并提出到 2020 年,建成一批特色鲜明的智慧城市.截至现今,全国 ...

  9. 移动/Web开发必备工具!DevExtreme v19.1.7火热发布

    DevExtreme Complete Subscription是性能最优的 HTML5,CSS 和 JavaScript 移动.Web开发框架,可以直接在Visual Studio集成开发环境,构建 ...

随机推荐

  1. scrapy框架初识及使用

    一.什么是Scrapy? Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名,非常强悍.所谓的框架就是一个已经被集成了各种功能(高性能异步下载,队列,分布式,解析,持久化等) ...

  2. Python - 本地文件读写(初级)

  3. ubuntu sublime text3 python 配置 sublime text3 python 配置

    ubuntu sublime text3 python 配置     1.安装sublime text 3 安装过程非常简单,在terminal中输入: sudo add-apt-repository ...

  4. vim的vimrc配置

    windows "# modified by Neoh set helplang=cn "使用中文帮助文档 set encoding=utf-8 "查看utf-8格式的帮 ...

  5. html5系列:form 2.0 新结构

    以往的一个form表单,结构比较死板,所有的form元素都必须处在<form>和</form>之间才有效,这会造成一些麻烦,比如说:像bootstrap这种使用<div& ...

  6. 小程序开发之一(使用fly进行http封装)

    原文地址:http://callmesoul.cn 下载fly js文件 fly小程序文档 /api/config.js 配置,主要配置全局的host url和request拦截和request拦截 ...

  7. EMS设置发送连接器和接收连接器邮件大小

    任务:通过EMS命令设置发送接收连接器和接收连接器的邮件大小限制值为50MB. 以Exchange管理员身份打开EMS控制台.在PowerShell命令提示符下. 键入以下命令设置接收-连接器的最大邮 ...

  8. Spring Security 一键接入验证码登录和小程序登录

    最近实现了一个多端登录的Spring Security组件,用起来非常丝滑,开箱即用,可插拔,而且灵活性非常强.我觉得能满足大部分场景的需要.目前完成了手机号验证码和微信小程序两种自定义登录,加上默认 ...

  9. python---二叉树广度优先和深度优先遍历的实现

    class Node(object): """结点""" def __init__(self, data): self.data = dat ...

  10. Java基础语法01——变量与运算符

    本文是对Java基础语法的第一部分的学习,包括注释:标识符的命名规则与规范:变量的数据类型分类以及转换:以及六种运算符(算术.赋值.比较.逻辑.三元和位运算符).