跳到主要内容

MVC 与 MVVM 模式详解

MVC 模式

MVC 是 Model-View-Controller 的缩写,是一种常见的软件架构模式。它将应用程序划分为三个部分: 模型(Model):负责管理和操作数据,执行数据的 CRUD(创建、读取、更新、删除)操作。 视图(View):负责数据的展示和用户交互。如果视图在服务端生成,就是服务端渲染;如果在客户端生成,就是客户端渲染。 控制器(Controller):负责处理业务逻辑,协调模型和视图之间的交互。前端通过请求 API 来调用控制器中的方法,执行相关的业务逻辑。

MVC 工作流程

  1. 前端发起一个异步请求,请求某个 URL。
  2. 服务端路由将请求映射到控制器的某个方法。
  3. 控制器方法执行业务逻辑,可能会查询或修改数据库中的数据。
  4. 控制器方法返回响应结果,将数据返回给前端。
  5. 前端接收到响应数据,更新视图。

MVC 的优点

MVC 的主要优点是实现了关注点分离,使得应用程序的结构更加清晰,不同模块之间耦合度低,方便开发和维护:

模型只需要关注数据,不需要关注其他。 视图只需要关注界面展示,不需要关注数据来源。 控制器只需要关注业务逻辑,不需要关注数据存储和界面展示的细节。

这种分层结构使得应用程序更容易扩展、修改和测试。比如可以单独修改视图而不影响模型和控制器。

一个简单的 MVC 实现示例

下面是一个使用原生 JavaScript 实现的简单 MVC 计算器示例

<div id="app"></div>

<script>
(function () {
// 初始化
function init() {
model.init();
view.render();
controller.init();
}

// 模型:管理数据
var model = {
data: {
a: 0,
b: 0,
s: '+',
r: 0,
},
init: function () {
var _this = this;
// 对data中的每个属性进行数据劫持
for (var k in _this.data) {
(function (k) {
Object.defineProperty(_this, k, {
get: function () {
return _this.data[k];
},
set: function (newValue) {
_this.data[k] = newValue;
// 数据变化时重新渲染视图
view.render({ [k]: newValue });
},
});
})(k);
}
},
};

// 视图:管理模板和渲染
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 (mutedData) {
if (!mutedData) {
// 初次渲染,将模板中的{{xxx}}替换为具体数据
this.template = this.template.replace(/\{\{(.*?)\}\}/g, function (node, key) {
return model[key.trim()];
});
document.querySelector(this.el).innerHTML = this.template;
} else {
// 数据变化后的重新渲染,直接修改相应dom元素的内容
for (var k in mutedData) {
document.querySelector('.cal-' + k).textContent = mutedData[k];
}
}
},
};

// 控制器:事件处理
var controller = {
init: function () {
var oCalInputs = document.querySelectorAll('.cal-input'),
oCalBtns = document.querySelectorAll('.cal-btn'),
inputItem,
btnItem;

// 绑定输入框事件
for (var i = 0; i < oCalInputs.length; i++) {
inputItem = oCalInputs[i];
inputItem.addEventListener('input', this.handleInput, false);
}

// 绑定按钮点击事件
for (var 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;
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');
}
},
};

init();
})();
</script>

这个示例中 模型 model 定义了数据和数据变化后要执行的操作 视图 view 定义了模板,以及如何渲染模板 控制器 controller 定义了事件处理函数,在特定事件发生时修改模型中的数据

通过这种分层设计,各个模块之间职责清晰,耦合度低,整个程序结构清晰,易于开发和维护。

MVVM 模式

MVVM 是 Model-View-ViewModel 的缩写,是一种基于 MVC 演变而来的架构模式。它引入了 ViewModel 这一层来简化视图和模型的交互。

MVVM 的组成部分

模型(Model):保存和处理数据,与 MVC 中的 Model 类似。 视图(View):负责界面展示,与用户进行交互。 视图模型(ViewModel):一个通过双向数据绑定连接视图和模型的对象。它封装了视图需要用到的属性和命令,视图通过数据绑定的方式与 ViewModel 进行通信。

MVVM 的工作原理

在 MVVM 中,视图和模型不直接通信,而是通过 ViewModel 来进行交互:

视图的变化会自动同步到 ViewModel,ViewModel 的变化也会自动同步到视图。 ViewModel 从模型获取数据,并对数据进行转换和处理,使其适合界面展示。 ViewModel 向模型发送命令,执行相应的业务逻辑。

这种通过 ViewModel 实现视图和模型的自动同步,使得它们之间的耦合度进一步降低。开发者只需要关注 ViewModel 的开发,而不需要手动操作 DOM。

MVVM 的优点

MVVM 模式相比 MVC 有以下优点

进一步降低了视图和模型的耦合度。视图和模型不直接通信,而是通过 ViewModel 来进行交互,使得它们之间的依赖关系更加清晰和松散。 简化了视图层的代码。开发者不需要手动操作 DOM,只需要维护 ViewModel 中的属性和方法即可,这使得代码更加简洁和易于维护。 提高了可测试性。ViewModel 抽象了视图的状态和行为,可以针对 ViewModel 编写单元测试,测试更加容易和可靠。

一个简单的 MVVM 实现示例

这里是一个使用 JavaScript 实现的简单 MVVM 示例,实现了数据的双向绑定:

// 一个简单的MVVM实现
function Vue(options) {
this.data = options.data;
var data = this.data;

observe(data);

var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
document.getElementById(id).appendChild(dom);
}

// 观察数据变化
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(function (key) {
defineReactive(obj, key, obj[key]);
});
}

function defineReactive(obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
get: function () {
return val;
},
set: function (newVal) {
val = newVal;
console.log('属性' + key + '已经被监听,现在值为:"' + newVal.toString() + '"');
},
});
}

// 将template转为fragment
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
var child;

while ((child = node.firstChild)) {
compile(child, vm);
flag.appendChild(child);
}
return flag;
}

// 编译模板
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/;
if (node.nodeType === 1) {
// 元素节点
var attr = node.attributes;
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue;
node.addEventListener('input', function (e) {
vm.data[name] = e.target.value;
});
node.value = vm.data[name];
node.removeAttribute('v-model');
}
}

new Watcher(vm, node, name, 'input');
}
if (node.nodeType === 3) {
// 文本节点
if (reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
new Watcher(vm, node, name, 'text');
}
}
}

// 观察者,当数据变化时更新视图
function Watcher(vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}

Watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
get: function () {
this.value = this.vm.data[this.name];
},
};

// 订阅器,管理多个观察者
function Dep() {
this.subs = [];
}

Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
},
};

Dep.target = null;

这个示例实现了以下功能:

通过Object.defineProperty对数据进行劫持,实现了数据的响应式。 将模板解析为抽象语法树,识别其中的指令和插值表达式。 为每个属性创建一个观察者 Watcher,当属性变化时通知观察者更新视图。 通过订阅器 Dep 管理多个观察者,实现了一对多的依赖关系。

虽然这只是一个简单的示例,但它展示了 MVVM 的核心原理数据的响应式和双向绑定。现代的 MVVM 框架如 Vue.js,就是在这个基础上进行了大量的扩展和优化,提供了更加完善和高效的解决方案。