1 原始的MVC (Model - View - Controller)
MVC (Model - View - Controller)架构是一种当前非常流行的架构模式(Architectural Pattern),被广泛使用于传统的桌面GUI(Graphical user interface)、Web应用和手机移动应用中。
- 1979年,MVC的概念被 Trygve Reenskaug 在
Smalltalk
中提出。这种架构模式,极大地降低了GUI应用程序的管理难度,而后被大量用于构建桌面和服务器端应用程序; - 从1996年后,MVC逐渐被广泛应用;
- 2002年,基于
Java
的Web应用框架Spring
引入了MVC的概念; - 2005年,基于
Python
的Django
和基于Ruby
的Rails
也引入了MVC的概念。
在最初以Smalltalk
实现中MVC概念中,MVC是由组合模式(Composite Pattern)、策略模式(Strategy Pattern)和观察者模式(Observer Pattern)三种设计模式组合而成的。
如图,实线代表方法调用,虚线代表事件通知(Event notification),通过观察者模式实现。
在前端框架的 MVC 中, View 代表UI界面(或者说 DOM),它关注于数据的显示和与用户的交互,Controller 中包含了前端页面的业务逻辑,而 Model中通常会包含 call backend APIs 的逻辑。
用户的输入操作本质上是传给了 View 层,而 View 会将这个输入操作传递给 Controller。此后可能会有两种情况:
- Controller 能自己处理,而不需要调用 Model 时,此时,Controller 会直接调用 View 的一个特定的函数。比如,这个用户的操作只需要触发网页上一个button 的隐藏(可以通过修改特定 DOM 元素的 style 来实现隐藏)。
- Controller 不能自己处理,此时,就需要调用 Model。在 Model 中,通常会调用backend APIs。比如,显示一个订单的详细内容。
Demo
这里有一个可以对数值进行加减操作的组件:上面显示数值,两个按钮可以对数值进行加减操作(这个操作需要实时地更新到 backend 的数据库中),操作后的数值会更新显示。
Model
Model层用来存储业务的数据,一旦数据发生变化,Model将会通过观察者模式,通知有关的View。
myapp.Model = function() {
var val = 0;
this.add = function(v) {
// 这里省略了调用后端 API 以将这个值更新到 DB 中的逻辑
if (val < 100) val += v;
};
this.sub = function(v) {
// 这里省略了调用后端 API 以将这个值更新到 DB 中的逻辑
if (val > 0) val -= v;
};
this.getVal = function() {
// 这里省略了调用后端 API 以读取这个值的逻辑
return val;
};
/* 观察者模式 */
var self = this,
views = [];
this.register = function(view) {
views.push(view);
};
this.notify = function() {
for(var i = 0; i < views.length; i++) {
views[i].render(self);
}
};
};
Model和View之间使用了观察者模式,因此View事先需要在此Model上注册,以观察Model。最终,当在 Model 中发生事件(数据变化)时,View 能得到通知。
View
View 和 Controller 之间使用了策略模式,这里View引入了Controller的实例来实现特定的响应策略,比如这个栗子中按钮的 click
事件:
myapp.View = function(controller) {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
// 更新数据到页面中
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
/* 绑定事件 */
$incBtn.click(controller.increase);
$decBtn.click(controller.decrease);
};
如果要实现不同的响应的策略只要用不同的Controller实例替换即可。
你会发现,用户的输入操作本质上是传给了View 层,只是 View 将这个操作直接传给了Controller。
Controller
Controller 是 Model 和 View 之间的纽带,MVC将响应机制封装在controller对象中,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。
myapp.Controller = function() {
var model = null,
view = null;
this.init = function() {
/* 初始化Model和View */
model = new myapp.Model();
view = new myapp.View(this);
/* View向Model注册,当Model更新就会去通知View啦 */
model.register(view);
model.notify();
};
/* 调用Model以更新数值,且 Model 通知 View 以更新数据 */
this.increase = function() {
model.add(1);
model.notify();
};
this.decrease = function() {
model.sub(1);
model.notify();
};
};
启动 MVC
这里我们实例化View并向对应的Model实例注册,当Model发生变化时就去通知View做更新,这里用到了观察者模式。
当我们执行应用的时候,使用Controller做初始化:
(function() {
var controller = new myapp.Controller();
controller.init();
})();
不足
View承接了部分Controller的功能,负责接收用户输入(但并不负责处理,而是无脑地传给 Controller)。
事实上,前端的View已经具备了独立处理用户事件的能力,如果每个事件都要流经Controller,势必增加复杂性。
同时,View 应该委托Controller来调用Model(这样 Model 对于 View 来说就是透明的),而不是Model 与 View 之间存在耦合。
你会发现,View 和 Controller 是一对一的关系。而事实上,前端的View其实已经具备了独立处理用户事件的能力,当每个事件都需要流经Controller时,Controller会变得十分臃肿。
在Backbone中,Backbone.View和Backbone.Router一起承担了Controller的责任。这就为MVC中controller的衍变埋下了伏笔。这其实是因为在原始的 MVC 中 View 和 Controller 一般是一一对应的,捆绑起来表示一个组件,View 与 Controller 间的过于紧密的连接让Controller的复用性成了问题,如果想多个View共用一个Controller该怎么办呢?这里有一个解决方案:MVP(Model-View-Presenter)。
2 优化后的MVC - MVP(Model-View-Presenter)
MVP(Model-View-Presenter)是MVC模式的改良,由IBM的子公司Taligent提出。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。
在原始的 MVC 里,View 是可以直接访问 Model 的,但MVP中,View并不能直接访问Model,而是 Presenter 给 View 提供接口,通过Presenter去更新Model,再通过观察者模式更新View。
与MVC相比,MVP模式通过解耦View和Model,完全分离 View 和 Model 使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。
而且,MVP 还解决了 Controller - View的捆绑关系,将进行改造,使View不仅拥有UI组件的结构,还拥有处理用户事件的能力,这样就能将 Controller 独立出来。为了对用户事件进行统一管理,View只负责将用户产生的事件传递给Controller,由Controller来统一处理,这样的好处是多个View可共用同一个Controller。此时的Controller也由组件级别上升到了应用级别。
Demo
Model
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
};
Model层依然是主要与业务相关的数据和对应处理数据的方法。
View
myapp.View = function() {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
this.init = function() {
var presenter = new myapp.Presenter(this);
$incBtn.click(presenter.increase);
$decBtn.click(presenter.decrease);
};
};
MVP定义了Presenter和View之间的接口,用户对View的操作都转移到了Presenter。比如这里可以让View暴露setter接口以便Presenter调用,待Presenter通知Model更新后,Presenter调用View提供的接口更新视图。
Presenter
myapp.Presenter = function(view) {
var _model = new myapp.Model();
var _view = view;
_view.render(_model);
this.increase = function() {
_model.add(1);
_view.render(_model);
};
this.decrease = function() {
_model.sub(1);
_view.render(_model);
};
};
Presenter作为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码需要对从View到Model和从Model到View的数据进行“手动同步”,这样Presenter显得很重,维护起来会比较困难。而且由于没有数据绑定,如果Presenter对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter也需要改动。
运行程序时,以View为入口:
(function() {
var view = new myapp.View();
view.init();
})();
MVVM
MVVM(Model-View-ViewModel)最初是由微软在使用Windows Presentation Foundation和SilverLight时定义的,2005年John Grossman在一篇关于Avalon(WPF 的代号)的博客文章中正式宣布了它的存在。
WPF Demo
如果你用过Visual Studio, 新建一个WPF Application,然后在“设计”中拖进去一个控件、双击后在“代码”中写事件处理函数、或者绑定数据源。就对这个MVVM有点感觉了。比如VS自动生成的如下代码:
<GroupBox Header="绑定对象">
<StackPanel Orientation="Horizontal" Name="stackPanel1">
<TextBlock Text="学号:"/>
<TextBlock Text="{Binding Path=StudentID}"/>
<TextBlock Text="姓名:"/>
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text="入学日期:"/>
<TextBlock Text="{Binding Path=EntryDate, StringFormat=yyyy-MM-dd}"/>
<TextBlock Text="学分:"/>
<TextBlock Text="{Binding Path=Credit}"/>
</StackPanel>
</GroupBox>
stackPanel1.DataContext = new Student() {
StudentID=20130501,
Name="张三",
EntryDate=DateTime.Parse("2013-09-01"),
Credit=0.0
};
其中最重要的特性之一就是数据绑定(Data-binding)。没有前后端分离,一个开发人员全搞定,一只手抓业务逻辑、一只手抓数据访问,顺带手拖放几个UI控件,绑定数据源到某个对象或某张表,一步到位。
MVVM
首先,view和model是不知道彼此存在的,同MVP一样,将view和model清晰地分离开来。
与 MVP 不同的是,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。
其次,view是对viewmodel的外在显示,与viewmodel保持同步,viewmodel对象可以看作是view的上下文。view绑定到viewmodel的属性上,如果viewmodel中的属性值变化了,这些新值通过数据绑定会自动传递给view。反过来viewmodel会暴露model中的数据和特定状态给view。 所以,view不知道model的存在,viewmodel和model也觉察不到view。事实上,model也完全忽略viewmodel和view的存在。这是一个非常松散耦合的设计。
这里我们使用Vue来完成这个栗子。
Vue.js Demo
Model
在MVVM中,我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为(格式化数据由View的负责),这里可以把它理解为一个类似 JSON 的数据对象。
var data = {
val: 0
};
View
和MVC/MVP不同的是,MVVM中的View通过使用模板语法来声明式的将数据渲染进DOM,当ViewModel对Model进行更新的时候,会通过数据绑定更新到View。写法如下:
<div id="myapp">
<div>
<span>{{ val }}rmb</span>
</div>
<div>
<button v-on:click="sub(1)">-</button>
<button v-on:click="add(1)">+</button>
</div>
</div>
ViewModel
ViewModel大致上就是MVC的Controller和MVP的Presenter了,也是整个模式的重点,业务逻辑也主要集中在这里,其中的一大核心就是数据绑定,后面将会讲到。
与MVP不同的是,没有了View为Presente提供的接口,之前由Presenter负责的View和Model之间的数据同步交给了ViewModel中的数据绑定进行处理,当Model发生变化,ViewModel就会自动更新;ViewModel变化,Model也会更新。
new Vue({
el: '#myapp',
data: data,
methods: {
add(v) {
if(this.val < 100) {
this.val += v;
}
},
sub(v) {
if(this.val > 0) {
this.val -= v;
}
}
}
});
整体来看,比MVC/MVP精简了很多,不仅仅简化了业务与界面的依赖,还解决了数据频繁更新(以前用jQuery操作DOM很繁琐)的问题。因为在MVVM中,View不知道Model的存在,ViewModel和Model也察觉不到View,这种低耦合模式可以使开发过程更加容易,提高应用的可重用性。
数据绑定(Data Binding)
在Vue中,使用了双向绑定技术(Two-Way-Data-Binding),就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。
不同的MVVM框架中,实现双向数据绑定的技术有所不同。目前一些主流的前端框架实现数据绑定的方式大致有以下几种:
- 数据劫持 (Vue)
- 发布-订阅模式 (Knockout、Backbone)
- 脏值检查 (Angular)
一些MV*框架
Backbone.js
Backbone通过提供模型Model、集合Collection、视图View赋予了Web应用程序分层结构,其中模型包含领域数据和自定义事件;集合Colection是模型的有序或无序集合,带有丰富的可枚举API; 视图可以声明事件处理函数。最终将模型、集合、视图与服务端的RESTful JSON接口连接。
Backbone在升级的过程中,去掉了controller,由view和router代替controller,view集中处理了用户事件(如click,keypress等)、渲染HTML模板、与模型数据的交互。Backbone的model没有与UI视图进行数据绑定,而是需要在view中自行操作DOM来更新或读取UI数据。Router为客户端路由提供了许多方法,并能连接到指定的动作(actions)和事件(events)。
Backbone是一个小巧灵活的库,只是帮你实现一个MVC模式的框架,更多的还需要自己去实现。适合有一定Web基础,喜欢原生JS去操作DOM(因为没有数据绑定)的开发人员。为什么称它为库,而不是框架,不仅仅是由于仅4KB的代码,更重要的是 使用一个库,你有控制权。如果用一个框架,控制权就反转了,变成框架在控制你。库能够给予灵活和自由,但是框架强制使用某种方式,减少重复代码。这便是Backbone与Angular的区别之一了。
至于Backbone属于MV*中的哪种模式,有人认为不是MVC,有人觉得更接近于MVP,事实上,它借用多个架构模式中一些很好的概念,创建一个运行良好的灵活框架。不必拘泥于某种模式。
// view:
var Appview = Backbone.View.extend({
// 每个view都需要一个指向DOM元素的引用,就像ER中的main属性。
el: '#container',
// view中不包含html标记,有一个链接到模板的引用。
template: _.template("<h3>Hello <%= who %></h3>"),
// 初始化方法
initialize: function(){
this.render();
},
// $el是一个已经缓存的jQuery对象
render: function(){
this.$el.html("Hello World");
},
// 事件绑定
events: {'keypress #new-todo': 'createTodoOnEnter'}
});
var appview = new Appview();
// model:
// 每个应用程序的核心、包含了交互数据和逻辑
// 如数据验证、getter、setter、默认值、数据初始化、数据转换
var app = {};
app.Todo = Backbone.model.extend({
defaults: {
title: '',
completed: false
}
});
// 创建一个model实例
var todo = new app.Todo({title: 'Learn Backbone.js', completed: false});
todo.get('title'); // "Learn Backbone.js"
todo.get('completed'); // false
todo.get('created_at'); // undefined
todo.set('created_at', Date());
todo.get('created_at'); // "Wed Sep 12 2012 12:51:17 GMT-0400 (EDT)"
// collection:
// model的有序集合,可以设置或获取model
// 监听集合中的数据变化,从后端获取模型数据、持久化。
app.TodoList = Backbone.Collection.extend({
model: app.Todo,
localStorage: new Store("backbone-todo")
});
// collection实例
var todoList = new app.TodoList()
todoList.create({title: 'Learn Backbone\'s Collection'});
// model实例
var model = new app.Todo({title: 'Learn models', completed: true});
todoList.add(model);
todoList.pluck('title');
todoList.pluck('completed');
Reference
- A note on DynaBook requirements
- Model View Controller History
- The Evolution of MVC
- Trygve Reenskaug - The original MVC reports
- Applications Programming in Smalltalk-80 (TM): How to use Model-View-Controller (MVC)
- Wikipedia - MVC
- Cocoa Core Competencies - MVC
- Concepts in Objective-C Programming - MVC
- ASP.NET MVC 4 Overview - Web Froms
- 浅析前端开发中的 MVC/MVP/MVVM 模式 - https://juejin.im/post/593021272f301e0058273468