前端开发的MVC/MVVM模式

asynchronous

MVC/MVVM都是常见的软件架构设计模式,它们是一种改进代码的组织方式,并不是一种设计模式,一个架构模式可能包含了多种设计模式。

Model & View

上面显示数值,两个按钮可以对数值进行加减操作,操作后的数值会更新显示。

Model

Model层用于封装业务逻辑相关的数据以及处理数据的方法。将用到的方法封装到Model中,并且定义了三个不同的操作方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myapp = {}
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
}
}

View

View作为视图层,主要负责数据的展示。

1
2
3
4
5
6
7
8
9
10
11
myapp.View = function() {
/* 视图元素 */
val $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');

/* 渲染数据 */
this.render = function(model) {
$num.text(model.getVal() + 'rmb')
}
}

通过Model&View完成了数据从模型层到视图层的逻辑。但是作为一个程序,还需要响应用户的操作,同步更新View和Model。所以MVC中引入了controller,它来定义界面对用户输入的响应方式,连接模型和视图。

MVC

实现代表方法调用,虚线代表事件通知。

用户对View的操作由Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,当Model发生变化时,通知相关视图进行更新。

Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
}

/* 观察者模式,订阅数据的变化 */
var self = this,
views = [];

this.register = function(view) {
views.push(view)
}

/* 通知View更新视图 */
this.notify = function() {
for(var i = 0;i<views.length;i++) {
views[i].render(self)
}
}
}

Model和View之间使用了观察者模式,View事先在Model上注册,进而观察Model,以便更新在Model上发生改变的数据。

View

View引入了Controller的实例来实现特定的响应策略,比如按钮中的click事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
myapp.View = function(controller) {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');

/* 将model数据更新到视图 */
this.render = function(model) {
$num.text(model.getVal() + 'rmb')
}

/* 绑定事件 */
$incBtn.click(controller.increase)
$decBtn.click(controller.decrease)
}

Controller

控制器是模型和视图之间的纽带,MVC将响应机制封装在Controller对象中,当用户和应用产生交互时,控制器中的事件触发器就开始工作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
myapp.Controller = function() {
var model = null,
view = null;

this.init = function() {
/* 初始化Model和View */
model = new myapp.Model()
view = new myapp.View(this)

/* View订阅Model数据,当数据变化时,通知View更新视图 */
model.register(view)
model.notify()
}

/* Model更新数值,继而通知View更新视图 */
this.increase = function() {
model.add(1)
model.notify()
}

this.decrease = function() {
model.sub(1)
model.notify()
}
}

这一部分使用了观察者模式,实例化View并向对应的Model实例注册,当Model发生变化时,就通知View更新。

当执行应用时,使用Controller初始化:

1
2
3
4
(function() {
var controller = new myapp.Controller()
controller.init()
})()

MVC的业务逻辑主要在Controller中,而View层也有独立处理用户事件的能力,当每个事件流经Controller时,这层会变得臃肿。并且MVC中View和Controller一般是一一对应的。视图和控制器之间的联系过于紧密,Controller的复用性成了问题。

MVVM

MVVM(Model-View-ViewModel),ViewModel指的是“Model of View”,视图的模型。

MVVM把View和Model的同步逻辑自动化,不再需要手动操作,而是交给数据绑定功能负责,只需要告诉它View显示的数据对应的是Model的哪个部分。

Model

在Model中只关注数据本身,没有其他任何行为(格式化数据由View完成)

1
2
3
var data = {
val: 0
}

View

MVVM中的View通过模板语法来声明式的将数据渲染进DOM,当ViewModel对Model进行更新时,会通过数据绑定更新到View。

1
2
3
4
5
6
7
8
9
<div id="app">
<div>
<span>{{ val }}</span>
</div>
<div>
<button @click="sub(1)">-</button>
<button @click="add(1)">+</button>
</div>
</div>

ViewModel

ViewModel相当于MVC的Controller。也就是业务逻辑。双向绑定可以使View和Model之间互相响应变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Vue({
el: '#app',
data: data,
methods: {
add(v) {
if(this.val < 100) {
this.val += v
}
},
sub(v) {
if(this.val > 0) {
this.val -= v
}
}
}
})

这样的模式可以将View和Model之间的耦合度降低,业务逻辑能从View层中剥离。

数据绑定

Vue运用了双向绑定技术,也就是ES5的Object.defineProperty。目前几种MVVM框架中实现数据绑定有这几种方法:

  • 数据劫持(Vue)
  • 发布订阅模式(Knockout、Backbone)
  • 脏值检测(Angular)

用一张网上比较流行的图来解释说明Vue的基本原理:

实现Vue的双向数据绑定分为三个模块:Observe、Compile、Watcher。Vue采用了发布订阅者的架构模式,运用ES5的Object.defineProperty劫持各属性的gettersetter,并在数据发生变动时通知订阅者,触发相应的监听回调。

  • Observe 数据监听
    负责对数据对象所有属性进行劫持,监听到数据变化后通知订阅者。

  • Compile 解析指令
    扫描模版,对指令进行解析,然后绑定事件。

  • Watcher 订阅者
    关联ObserveCompile,订阅并收到属性变动的通知,执行解析指令上的回调函数。Update是其自身的方法,用于执行Compile中绑定的回调,更新视图。

总结

MV*目的都是把数据、业务逻辑、表现层这三部分解耦,分离关注点,有利于测试和维护。业务逻辑不关心底层数据的读写,这些数据以对象的形式呈现给业务逻辑层。

曾经我也很困惑是应该追逐热点,完全使用已经日臻完善的所谓“新技术”,还是固守以前的成果,用已经成熟的知识去提供解决方案。其实这两点并不完全冲突,应该思考业务场景和开发需求,不同需求自然有合适的方案。

我们选择它,使用它,就代表了认可它的思想,相信它能提供开发效率,而不仅仅是因为大家都在追逐它们。