MVC、MVVM模式
MVC
上个世纪70年代,美国施乐帕克研究中心,就是那个发明图形用户界面(GUI)的公司,开发了Smalltalk编程语言,并开始用它编写图形界面的应用程序。
到了Smalltalk-80这个版本的时候,一位叫Trygve Reenskaug的工程师为Smalltalk设计了MVC(Model-View-Controller)这种架构模式,极大地降低了GUI应用程序的管理难度,而后被大量用于构建桌面和服务器端应用程序
如图,实线代表方法调用,虚线代表事件通知。
MVC允许在不改变视图的情况下改变视图对用户输入的响应方式,用户对View的操作交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图进行更新。
Model
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; }; /* 观察者模式 */ 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和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实例替换即可。
Controller
控制器是模型和视图之间的纽带,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更新数值并通知View更新视图 */ this.increase = function() { model.add(1); model.notify(); }; this.decrease = function() { model.sub(1); model.notify(); }; };
这里我们实例化View并向对应的Model实例注册,当Model发生变化时就去通知View做更新,这里用到了观察者模式。
当我们执行应用的时候,使用Controller做初始化:
(function() { var controller = new myapp.Controller(); controller.init(); })();
MVP
MVP(Model-View-Presenter)是MVC模式的改良,由IBM的子公司Taligent提出。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。
虽然在MVC里,View是可以直接访问Model的,但MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。
与MVC相比,MVP模式通过解耦View和Model,完全分离视图和模型使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。
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)最早由微软提出。ViewModel指 "Model of View"——视图的模型。这个概念曾在一段时间内被前端圈热炒,以至于很多初学者拿jQuery和Vue做对比...
MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。
这里我们使用Vue来完成这个栗子。
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; } } } });
数据绑定
在Vue中,使用了双向绑定技术(Two-Way-Data-Binding),就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。
不同的MVVM框架中,实现双向数据绑定的技术有所不同。目前一些主流的前端框架实现数据绑定的方式大致有以下几种:
- 数据劫持 (Vue)
- 发布-订阅模式 (Knockout、Backbone)
- 脏值检查 (Angular)
我们这里主要讲讲Vue。
Object.defineProperty()
方法来劫持(监控)各属性的 getter
、setter
,并在数据(对象)发生变动时通知订阅者,触发相应的监听回调。
-
Observer 数据监听器
负责对数据对象的所有属性进行监听(数据劫持),监听到数据发生变化后通知订阅者。 -
Compiler 指令解析器
扫描模板,并对指令进行解析,然后绑定指定事件。 -
Watcher 订阅者
关联Observer和Compile,能够订阅并收到属性变动的通知,执行指令绑定的相应操作,更新视图。Update()是它自身的一个方法,用于执行Compile中绑定的回调,更新视图。
数据劫持
一般对数据的劫持都是通过Object.defineProperty方法进行的,Vue中对应的函数为 defineReactive
,其普通对象的劫持的精简版代码如下:
var foo = { name: 'vue', version: '2.0' } function observe(data) { if (!data || typeof data !== 'object') { return } // 使用递归劫持对象属性 Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }) } function defineReactive(obj, key, value) { // 监听子属性 比如这里data对象里的 'name' 或者 'version' observe(value) Object.defineProperty(obj, key, { get: function reactiveGetter() { return value }, set: function reactiveSetter(newVal) { if (value === newVal) { return } else { value = newVal console.log(`监听成功:${value} --> ${newVal}`) } } }) } observe(foo) foo.name = 'angular' // “监听成功:vue --> angular”
上面完成了对数据对象的监听,接下来还需要在监听到变化后去通知订阅者,这需要实现一个消息订阅器 Dep
,Watcher通过 Dep
添加订阅者,当数据改变便触发 Dep.notify()
,Watcher调用自己的 update()
方法完成视图更新。