第四十五课:MVC,MVP,MVVM的区别

时间:2023-03-09 09:34:31
第四十五课:MVC,MVP,MVVM的区别

前端架构从MVC到MVP,再到MVVM,它们都有不同的应用场景。但MVVM已经被证实为界面开发最好的方案了。

MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负 责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 MVC模型关注的是Model的不变,所以,在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。

在MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的 View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,即重用!

在MVP里,应用程序的逻辑主要在Presenter来实现,其中的View是很薄的一层。在这个过程中,View是很简单的,能够把信息显示清楚就可以了。在后面,根据需要再随便更改View, 而对Presenter没有任何的影响了。 如果要实现的UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在View和Presenter之间放置一个Adapter。由这个 Adapter来访问Model和View,避免两者之间的关联。而同时,因为Adapter实现了View的接口,从而可以保证与Presenter之间接口的不变。这样就可以保证View和Presenter之间接口的简洁,又不失去UI的灵活性。 在MVP模式里,View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容,绝不容许直接访问 Model--这就是与MVC很大的不同之处。

MVVM在概念上是真正将页面与数据逻辑分离的模式,它把数据绑定工作放到一个JS里去实现,而这个JS文件的主要功能是完成数据的绑定,即把model绑定到UI的元素上。

大家都知道,我们前端使用MVC或MVP模式进行开发时,这个V与传统意义上的V是不一样的。在后端,这只是字符串的拼接,在前端,还涉及到DOM操作。即便你加入了模板,你也要将script标签中的模板内容与后端返回的数据进行结合,生成一个符合HTML结构的字符串,最后,通过innerHTML转换为页面节点,显示出来。而这些操作,我们可以通过MVVM中的动态模板搞定。它的原理大概是:动态模板在扫描之后,会得到所有要处理的节点的引用,这也意味着,以后我们要做一小部分的更新,不用像静态模板那样大规模替换,而是细化到每一个元素节点,特性节点或文本节点。这就是所谓的“最小化刷新”技术。一般的,只有ms-if等少量绑定才会影响到元素节点那一层面,更多的时候, 我们是在刷新特性节点的value值,文本节点的data值,这也意味着,我们的刷新不会引起reflow。加之,能得到元素节点本身,我们就可以轻松实现绑定事件,操作样式,修改属性等功能。这也是为什么大多数MVVM框架选择动态模板的缘故,jQuery原来可以做的,我们全部通过绑定属性或定界符在HTML里搞定。 这也意味着,我们实现了完美的分层架构,JS里面是纯粹的模型层(包括model与viewmodel),HTML里是视图层。

此外,MVVM另一个重要特性,双向绑定。它更方便你同时维护页面上都依赖于某个字段的N个区域,而不用手动更新它们。

有人做过测试:使用Angular(MVVM)代替Backbone(MVC)来开发,代码可以减少一半。

MVVM算一个很新的东西,后端诞生于2005年,前端诞生于2010年发布的knockout框架。目前主要有knockout.js,ember.js,angular.js,win.js,kendoui等。

了解完这些概念后,我们来看两个用Backbone写的例子,我们通过例子来详细的了解下前端MVC是如何实现的:

1 <!DOCTYPE html>
2 <html xmlns="http://www.w3.org/1999/xhtml">
3 <head>
4 <meta charset="utf-8" />
5 <title></title>
6 <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
7 <link rel="Stylesheet" type="text/css" href="res/style/main2.css" />
8 <link rel="Stylesheet" type="text/css" href="res/style/tuan.css" />
9 <style> .pro_list_rank { margin: 5px 0; padding-right: 22px; }
10 .figcaption span { text-align: center; }
11 .blog_item {}
12 .blog_item img { width: 48px; height; 48px; margin: 4px; padding: 1px; float: left; border: 1px solid #CCC; }
13
14 .blog_item .item_footer { color: #757575; font-size: 0.86em; }
15 a { color: #005A94; }
16 .tab_hotel { border-left: 1px solid #2B97E2; }
17 .cont_wrap .content { background-color: White; padding: 5px 10px; }
18 img { max-width: 98%; }</style>
19 </head>
20 <body>
21 <div class="main-frame">
22 <div class="main-viewport" id="main-viewport">
23 </div>
24 </div>
25 <script type="text/template" id="index-template">
26 <header>
27 <b class="icon_home i_bef" id="js_home"></b>
28 <h1>
29 博客园</h1>
30
31 <i id="js_return" class="returnico"></i>
32 </header>
33 <section class="cont_wrap">
34 <div id="post"></div>
35 <ul class="pro_list" id="lstbox">
36 </ul>
37 </section>
38 <ul class="tab_search fix_bottom" id="sort">
39 <li class="tabcrt" attr="updated">时间</li>
40 <li class="tab_hotel" attr="diggs">推荐</li>
41 <li class="tab_hotel" attr="views">阅读</li>
42 <li class="tab_hotel" attr="comments">评论</li>
43 </ul>
44 </script>
45 <script type="text/template" id="item-template">
46 <li class="arr_r orderItem" data-id="<%=id %>" data-index = "<%=index %>">
47 <article class="blog_item">
48 <h3>
49 <a href="<%=link.href %>" target="_blank">
50 <%=title.value || '无题' %></a>
51 </h3>
52 <div class="author pro_list_rank">
53 <%if(author.avatar){ %>
54 <a href="<%=author.uri %>" target="_blank">
55 <img src="<%=author.avatar %>">
56 </a>
57 <%} %>
58 <%=summary.value %>
59 </div>
60 <div class="item_footer">
61 <a href="<%=author.uri %>" class="lightblue">Scut</a>
62 <%=published %>
63 <a href="<%=link.href %>" title="2013-08-21 15:21" class="gray">评论(<%=comments %>)</a>
64 <a href="<%=link.href %>" class="gray">阅读(<%=views %>)</a> <span class="price1">推荐(<%=diggs %>)</span></div>
65 </article>
66 </li>
67 </script>
68 <script type="text/template" id="detail-template">
69 <section class="cont_wrap" >
70 <article class="content">
71 <h1>
72 <a href="#"><%=title.value %></a></h1>
73 <div style=" text-align: right; ">
74 <time pubdate="pubdate" value="2013-04-15"><%=published %></time><br /><span>阅读(<%=views %>)
75 评论(<%=comments %>)</span>
76 </div>
77 <p><%=value %></p>
78 </article>
79 </section>
80 </script>
81 <script src="libs/jquery.js" type="text/javascript"></script>
82 <script src="libs/underscore.js" type="text/javascript"></script>
83 <script src="libs/backbone.js" type="text/javascript"></script>
84 <script type="text/javascript" src="libs/backbone.localStorage.js"></script>
85 <script type="text/javascript">
86 //模型
87 var PostModel = Backbone.Model.extend({
88
89 });
90
91 //模型集合
92 var PostList = Backbone.Collection.extend({
93 model: PostModel,
94 parse: function (data) {
95
96 return (data && data.feed && data.feed.entry) || {}
97 },
98 setComparator: function (type) {
99 this.comparator = function (item) {
100 return Math.max(item.attributes[type]);
101 }
102 }
103 });
104 //视图,文章内容的视图
105 var Detail = Backbone.View.extend({
106 el: $('#main-viewport'),
107 template: _.template($('#index-template').html()),
108 detail: _.template($('#detail-template').html()),
109 initialize: function (app) {
110 this.app = app;
111 this.$el.html(this.template());
112 this.wrapper = $('#lstbox');
113 this.render();
114 },
115 render: function () {
116 var scope = this;
117 var id = this.app.id;
118
119 var param = { url: 'http://wcf.open.cnblogs.com/blog/post/body/' + id }
120
121 var model = this.app.model;
122
123 $.get('Handler.ashx', param, function (data) {
124 (typeof data === 'string') && (data = $.parseJSON(data));
125 if (data && data.string) {
126 //此处将content内容写入model
127 model.set('value', data.string.value);
128 scope.wrapper.html(scope.detail(model.toJSON()));
129 }
130 });
131
132 },
133 events: {
134 'click #js_return': function () {
135 this.app.forward('index')
136 }
137 }
138 });
139 //视图,文章列表的视图
140 var Index = Backbone.View.extend({
141 el: $('#main-viewport'),
142 template: _.template($('#index-template').html()),
143 itemTmpt: _.template($('#item-template').html()),
144
145 events: {
146 'click #sort': function (e) {
147 var el = $(e.target);
148 var type = el.attr('attr');
149 this.list.setComparator(type);
150 this.list.sort();
151 },
152 'click .orderItem': function (e) {
153 var el = $(e.currentTarget);
154 var index = el.attr('data-index');
155 var id = el.attr('data-id');
156 var model = this.list.models[index];
157 this.app.model = model;
158 this.app.id = id;
161 this.app.forward('detail');
175 }
176 },
177 initialize: function (app) {
178 this.app = app;
179
180 //先生成框架html
181 this.$el.html(this.template());
182 this.post = this.$('#post');
183
184 var scope = this;
185 var curpage = 1;
186 var pageSize = 10;
187 this.list = new PostList();
188 this.list.url = 'Handler.ashx?url=http://wcf.open.cnblogs.com/blog/sitehome/paged/' + curpage + '/' + pageSize;
189 this.list.fetch({
190 success: function () {
191 scope.render();
192 }
193 });
194 this.wrapper = $('#lstbox');
195
196 this.listenTo(this.list, 'all', this.render);
197
198 },
199 render: function () {
200
201 var models = this.list.models;
202 var html = '';
203 for (var i = 0, len = models.length; i < len; i++) {
204 models[i].index = i;
205 html += this.itemTmpt(_.extend(models[i].toJSON(), { index: i }));
206 }
207 this.wrapper.html(html);
208 var s = '';
209 }
210 });
215 var App = Backbone.Router.extend({
216 routes: {
217 "": "index", // #index
218 "index": "index", // #index
219 "detail": "detail" // #detail
220 },
221 index: function () {
222 var index = new Index(this.interface);
223
224 },
225 detail: function () {
226 var detail = new Detail(this.interface);
227
228 },
229 initialize: function () {
231 },
232 interface: {
233 forward: function (url) {
234 window.location.href = ('#' + url).replace(/^#+/, '#');
235 }
236
237 }
240 });
242 var app = new App();
243 Backbone.history.start();
245 var s = '';
247 </script>
248 </body>
249 </html>

我们来分析这段代码时,只需要看js代码。代码的一开始,我们先定义了一个模型PostModel,这个模型相当于后台返回的一条数据。然后定义了一个PostList集合,它里面的每一项就是模型PostModel。集合PostList有两个方法,一个是parse方法,它用于解析后台返回的数据,会自动调用,因此你可以重写此方法,改变后台数据的表现形式。第二个方法setComparator用来设置模型集合排序时,使用的比较方法(比如:模型集合PostList.sort(),会对里面的模型进行排序,这时排序调用的比较方法就是comparator)。

接下来,定义了一个视图Detail,此视图是用来显示文章内容的。由于它只显示一篇文章,所以它只操作一个模型,这里就是操作PostModel。

然后,定义了一个视图Index,此视图是用来显示文章列表的,由于它显示很多文章的标题,因此它操作的就是模型集合PostList。

最后定义了一个路由App,我们也可以叫它Controller。它主要通过Hash值的变化,来改变视图的。

我们总共定义了两个视图,一个模型,一个集合,一个路由。那我们如何使用他们呢,首先初始化一个路由对象,然后启动路由功能。路由的使用,我们不仅需要初始化一个对象,而且必须调用Backbone.history.start()。

当用户输入url访问这个页面的时候,比如:www.chaojidan.com,这时没有hash值,因此会调用路由中的index方法,这时,就会初始化Index视图,并把路由中的interface对象传进这个视图。实例化Index视图时,就会调用Index的initialize方法,在此方法中,又会实例化一个集合PostList对象list,然后通过这个集合对象向后台请求数据,数据返回后,就会存储在集合对象list中,这时就会调用视图Index的render方法,此方法,就会把集合list中的数据全部显示出来。同时,视图中的events对象,就会自动绑定一些事件。

当我们点击.orderItem这个元素(此元素就是文章列表)时,就会执行回调方法,此回调方法,就会让页面上显示此文章的内容,也就是视图的变化。在这个回调方法中,会调用路由的forward方法,此方法就会改变页面的url,这时url会变成www.chaojidan.com#detail。由于hash值变化了,这时就会调用路由中的回调方法detail,而此方法就会实例化一个detail视图对象。

在detail视图中,就会去获取你点击的文章的内容,然后显示在页面上。

大家看懂这个代码后,再来考虑下,它的MVC模式是如何体现的?

首先model模型PostModel,它对应后台的一条数据,collection集合PostList,它对应后台的多条数据。与后台交互的是collection,集合的功能就是从后台请求数据,然后把数据进行解析,每一条数据就是一个model。

然后视图Index是用来显示集合的的数据,也就是显示多个model。视图detail用来显示单条数据,这里的数据是文章内容,而collection集合中的数据是文章标题,也就是说在Index视图中,模型model只是一个文章标题,而在detail视图中,模型model是文章的内容。这里的视图是用模板的形式把数据套进去,然后添加到页面上的,每次模型的数据变化,都会进行模板重新组装,即便是改变了一个数据,就要把整个模板进行组装,是不是有点浪费呢?

视图之间的切换,是通过router路由来实现的,因为视图中绑定了一些方法,比如在文章列表中绑定了click事件,当你点击文章列表中的一项时(也就是想看此文章的内容时),就会改变hash值(改变hash的值,不会请求服务器),这时因为启动了路由功能,所以就会调用此hash值对应的方法,然后初始化detail视图,此视图,就会去后台取此文章的内容,然后显示在此页面上。

如果公司中的项目用Backbone来实现,然后加上sea.js来进行模块化开发,那么,我们可以在init.js中,引入路由这个模块,然后初始一个路由对象,并调用Backbone.historty.start()来启动此路由。而这个路由模块中,定义了一个跟菜单选项相对应的路由表,比如:第一个菜单,就是默认显示的,那么,它的hash值对应"",当用户访问www.chaojidan.com时,就会调用此hash对应的回调方法,然后加载此菜单需要的js文件,也就是模块(这里面其实就是定义了View和Model),这里通过sea.js中的require.async方法加载,加载成功后,就会实例化此View和Model,在View中就会进行初始化操作,然后就会通过model向服务器请求数据,最后通过View显示在页面中。

点击一个菜单,就会改变hash的值,就会执行相对应的回调方法, 然后就会加载相对应的js文件(模块),最后就会请求服务器返回数据,把数据显示在页面上。

这里的js文件(模块),只有你点击相对应的菜单栏时,才会去后台下载并解析,是否能够很好的处理同时加载太多js文件导致的页面假死情况。

这里面需要注意的是在js文件(模块)中,我们的initialize方法,一开始就需要调用thie.el.off()方法,此方法,就是取消此视图中的之前所有的事件绑定,以防你重复绑定。

这一课,在概念上,知道了MVC和MVVM的区别,然后从实际上知道了MVC的开发模式。

下一课,我们将从实际上来讲解MVC和MVVM的区别。

加油!