在單頁應用中,view與view之間的通訊機制一直是一個重點,由於單頁應用的全部操做以及狀態管理所有發生在一個頁面上javascript
沒有很好的組織的話很容易就亂了,就算表面上看起來沒有問題,事實上會有各類隱憂,各類坑等着你去跳css
最初就沒有必定理論上的支撐,極有多是這麼一種狀況:html
① 需求下來了,搞一個demo作交待java
② 發現基本知足很滿意,因而直接在demo中作調整git
上面的作法自己沒有什麼問題,問題發生在後期github
③ 在demo調整後應用到了實際業務中,發現不少地方有問題,因而見一個坑解決一個坑bootstrap
④ 到最後感受整個框架零零散散,有不少if代碼,有不少代碼不太懂意思,可是一旦移除就報錯閉包
這個時候咱們就想到了重構,重構過程當中就會發現最初的設計,或者說整個框架的基礎有問題,因而就提出推翻重來mvc
如果時間上容許,還能夠,可是每每重構過程當中,會多一些不按套路出牌的同窗,將API接口給換了,這一換全部的業務系統所有崩潰app
因此說,新的框架會對業務線形成壓力,會提升測試與編碼成本,因而就回到了咱們上篇博客的問題
一些同窗認爲,以這種方式寫UI組件過於麻煩,可是咱們實際的場景是這樣的
咱們全部的UI 組件可能會由一個UIAbstractView繼承而來,這樣的繼承的好處是:
① 咱們每一個UI組件都會遵循一個事件的流程作編寫,好比:
onCreate->preShow->show->afterShow->onHide->destroy (簡單說明便可)
因而咱們想在每個組件顯示前作一點操做的話,咱們能夠統一寫到AbstractView中去(事實上咱們應該寫到businessView中)
② 在AbstractView中咱們能夠維護一個共用的閉包環境,這個閉包環境被各個UI組件共享,因而UI與UI之間的通訊就變成了實例的操做而不是dom操做
固然,事實上經過DOM的操做,選擇器,id的什麼方式可能同樣能夠實現相同的功能,可是正如上面所言,這種方式會有隱憂
事實上是對UI組件編寫的一種約束,沒有約束的組件作起來固然簡單
可是有了約束的組件的狀態處理才能被統一化,由於
單頁應用的內存清理、狀態管理纔是真正的難點
PS:此文僅表明我的淺薄想法,有問題請指出
其實所謂消息通訊,不過是一種發佈訂閱的關係,又名觀察者;觀察者有着一對多的關係
多個對象觀察同一個主體對象,如果主體對象發生變化便會通知全部觀察者變化,事實上觀察者自己又能夠變成主體對象,因此多對多的關係偶爾不可避免
還有一些時候觀察者也可能變成本身,本身的某些狀態會被觀察
其實前面扯那麼多有的沒的不如來一個代碼,在Backbone中有一段代碼簡單實現了這個邏輯
1 var Events = Backbone.Events = { 2 on: function (name, callback, context) { 3 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 4 this._events || (this._events = {}); 5 var events = this._events[name] || (this._events[name] = []); 6 events.push({ callback: callback, context: context, ctx: context || this }); 7 return this; 8 }, 9 10 off: function (name, callback, context) { 11 var retain, ev, events, names, i, l, j, k; 12 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 13 if (!name && !callback && !context) { 14 this._events = {}; 15 return this; 16 } 17 18 names = name ? [name] : _.keys(this._events); 19 for (i = 0, l = names.length; i < l; i++) { 20 name = names[i]; 21 if (events = this._events[name]) { 22 this._events[name] = retain = []; 23 if (callback || context) { 24 for (j = 0, k = events.length; j < k; j++) { 25 ev = events[j]; 26 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 27 (context && context !== ev.context)) { 28 retain.push(ev); 29 } 30 } 31 } 32 if (!retain.length) delete this._events[name]; 33 } 34 } 35 36 return this; 37 }, 38 39 trigger: function (name) { 40 if (!this._events) return this; 41 var args = slice.call(arguments, 1); 42 if (!eventsApi(this, 'trigger', name, args)) return this; 43 var events = this._events[name]; 44 var allEvents = this._events.all; 45 if (events) triggerEvents(events, args); 46 if (allEvents) triggerEvents(allEvents, arguments); 47 return this; 48 }, 49 };
這是一段簡單的邏輯,也許他的主幹還不全,咱們這裏如果作一個簡單的實現的話就會變成這個樣子:
1 var Events = {}; 2 Events.__events__ = {}; 3 4 Events.addEvent = function (type, handler) { 5 if (!type || !handler) { 6 throw "addEvent Parameter is not complete!"; 7 } 8 var handlers = Events.__events__[type] || []; 9 handlers.push(handler); 10 Events.__events__[type] = handlers; 11 }; 12 13 Events.removeEvent = function (type, handler) { 14 if (!type) { 15 throw "removeEvent parameters must be at least specify the type!"; 16 } 17 var handlers = Events.__events__[type], index; 18 if (!handlers) return; 19 if (handler) { 20 for (var i = Math.max(handlers.length - 1, 0); i >= 0; i--) { 21 if (handlers[i] === handler) handlers.splice(i, 1); 22 } 23 } else { 24 delete handlers[type]; 25 } 26 }; 27 28 Events.trigger = function (type, args, scope) { 29 var handlers = Events.__events__[type]; 30 if (handlers) for (var i = 0, len = handlers.length; i < len; i++) { 31 typeof handlers[i] === 'function' && handlers[i].apply(scope || this, args); 32 } 33 };
整個程序邏輯以下:
① 建立一個events對象做爲消息存放點
② 使用on放events中存放一個個事件句柄
③ 在知足必定條件的狀況下,觸發相關的事件集合
簡單而言,以IScroll爲例,他在構造函數中定義了默認的屬性:
this._events = {};
而後提供了最簡單的註冊、觸發接口
1 on: function (type, fn) { 2 if (!this._events[type]) { 3 this._events[type] = []; 4 } 5 6 this._events[type].push(fn); 7 }, 8 9 _execEvent: function (type) { 10 if (!this._events[type]) { 11 return; 12 } 13 14 var i = 0, 15 l = this._events[type].length; 16 17 if (!l) { 18 return; 19 } 20 21 for (; i < l; i++) { 22 this._events[type][i].call(this); 23 } 24 },
由於IScroll中涉及到了自身與滾動條之間的通訊,因此是個很好的例子,咱們看看IScroll的使用:
他對自身展開了監聽,如果發生如下事件便會觸發響應方法
1 _initIndicator: function () { 2 //滾動條 3 var el = createDefaultScrollbar(); 4 this.wrapper.appendChild(el); 5 this.indicator = new Indicator(this, { el: el }); 6 7 this.on('scrollEnd', function () { 8 this.indicator.fade(); 9 }); 10 11 var scope = this; 12 this.on('scrollCancel', function () { 13 scope.indicator.fade(); 14 }); 15 16 this.on('scrollStart', function () { 17 scope.indicator.fade(1); 18 }); 19 20 this.on('beforeScrollStart', function () { 21 scope.indicator.fade(1, true); 22 }); 23 24 this.on('refresh', function () { 25 scope.indicator.refresh(); 26 }); 27 28 },
好比在每次拖動結束的時候,皆會拋一個事件出來
that._execEvent('scrollEnd');
他只負責拋出事件,而後具體執行的邏輯其實早就寫好了,他沒必要關注起作了什麼,由於那個不是他須要關注的
再說回頭,IScroll的事件還能夠被用戶註冊,因而用戶即可以在各個事件點封裝本身想要的邏輯
好比IScroll每次移動的結果都會是一個步長,即可以在scrollEnd觸發本身的邏輯,可是因爲iScroll最後的移動值爲一個局部變量,因此這裏可能須要將其中的newY定製於this上
如IScroll的消息機制只會用於自身,如Backbone的Model、View層各自維護着本身的消息中心,在一個單頁框架中,此消息樞紐事實上能夠只有一個
好比頁面標籤的View能夠是一個消息羣組
UI組件能夠是一個消息羣組
Model層也能夠是一個消息羣組
......
因此這個統一的消息中心,事實上咱們一個框架能夠提供一個單例,讓各個系統去使用
7 Dalmatian = {}; 8 9 Dalmatian.MessageCenter = _.inherit({ 10 initialize: function () { 11 //框架全部的消息皆存於此 12 /* 13 { 14 view: {key1: [], key2: []}, 15 ui: {key1: [], key2: []}, 16 model: {key1: [], key2: []} 17 other: {......} 18 } 19 */ 20 this.msgGroup = {}; 21 }, 22 23 _verify: function (options) { 24 if (!_.property('namespace')(options)) throw Error('必須知道該消息的命名空間'); 25 if (!_.property('id')(options)) throw Error('該消息必須具有key值'); 26 if (!_.property('handler')(options) && _.isFunction(options.handler)) throw Error('該消息必須具有事件句柄'); 27 }, 28 29 //註冊時須要提供namespace、key、事件句柄 30 //這裏能夠考慮提供一個message類 31 register: function (namespace, id, handler) { 32 var message = {}; 33 34 if (_.isObject(namespace)) { 35 message = namespace; 36 } else { 37 message.namespace = namespace; 38 message.id = id; 39 message.handler = handler; 40 41 } 42 43 this._verify(message); 44 45 if (!this.msgGroup[message.namespace]) this.msgGroup[message.namespace] = {}; 46 if (!this.msgGroup[message.namespace][message.id]) this.msgGroup[message.namespace][message.id] = []; 47 this.msgGroup[message.namespace][message.id].push(message.handler); 48 }, 49 50 //取消時候有所不一樣 51 //0 清理全部 52 //1 清理整個命名空間的事件 53 //2 清理一個命名空間中的一個 54 //3 清理到具體實例上 55 unregister: function (namespace, id, handler) { 56 var removeArr = [ 57 'clearMessageGroup', 58 'clearNamespace', 59 'clearObservers', 60 'removeObserver' 61 ]; 62 var removeFn = removeArr[arguments.length]; 63 64 if (_.isFunction(removeFn)) removeFn.call(this, arguments); 65 66 }, 67 68 clearMessageGroup: function () { 69 this.msgGroup = {}; 70 }, 71 72 clearNamespace: function (namespace) { 73 if (this.msgGroup[namespace]) this.msgGroup[namespace] = {}; 74 }, 75 76 clearObservers: function (namespace, id) { 77 if (!this.msgGroup[namespace]) return; 78 if (!this.msgGroup[namespace][id]) return; 79 this.msgGroup[namespace][id] = []; 80 }, 81 82 //沒有具體事件句柄便不能被移除 83 removeObserver: function (namespace, id, handler) { 84 var i, len, _arr; 85 if (!this.msgGroup[namespace]) return; 86 if (!this.msgGroup[namespace][id]) return; 87 _arr = this.msgGroup[namespace][id]; 88 89 for (i = 0, len = _arr.length; i < len; i++) { 90 if (_arr[i] === handler) _arr[id].splice(i, 1); 91 } 92 }, 93 94 //觸發各個事件,事件句柄所處做用域需傳入時本身處理 95 dispatch: function (namespace, id, data, scope) { 96 var i, len, _arr; 97 98 if (!(namespace && id)) return; 99 100 if (!this.msgGroup[namespace]) return; 101 if (!this.msgGroup[namespace][id]) return; 102 _arr = this.msgGroup[namespace][id]; 103 104 for (i = 0, len = _arr.length; i < len; i++) { 105 if (_.isFunction(_arr[i])) _arr[i].call(scope || this, data); 106 } 107 } 108 109 }); 110 111 Dalmatian.MessageCenter.getInstance = function () { 112 if (!this.instance) { 113 this.instance = new Dalmatian.MessageCenter(); 114 } 115 return this.instance; 116 }; 117 118 Dalmatian.MSG = Dalmatian.MESSAGECENTER = Dalmatian.MessageCenter.getInstance();
完了這塊咱們怎麼使用了,這裏回到咱們的alert與日曆框,讓咱們實現他們之間的通訊
咱們實現這樣的效果,點擊alert框時,顯示一個時間,而且日曆上將此日期標紅
PS:每次一到這個時間久累了,代碼未作整理
① 咱們在calendar實例化的時候便作事件註冊(訂閱)
//事件註冊點,應該單獨封裝 Dalmatian.MSG.register('ui', 'alertShow', $.proxy(function (data) { var s = ''; }, this));
② 在每次設置message內容時候便拋出事件
1 set: function (options) { 2 _.extend(this.adapter.datamodel, options); 3 // this.adapter.datamodel.content = options.content; 4 this.adapter.notifyDataChanged(); 5 6 if (options.content) { 7 Dalmatian.MSG.dispatch('ui', 'alertShow', options.content); 8 } 9 10 },
因而便有了這樣的效果,每次設置值的時候,我這裏都會被觸發
並且這裏的this指向的是calendar,因此咱們這裏能夠作處理,因爲時間緣由,我這裏便亂幹了
能夠看到,每次操做後,calendar獲得了更新,可是因爲我這裏是直接操做的dom未作datamodel操做,因此沒有作狀態保存,第二次其實是該刷新的
這裏咱們暫時無論
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>ToDoList</title> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css"> 8 <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css"> 9 <link href="../style/main.css" rel="stylesheet" type="text/css" /> 10 <style type="text/css"> 11 .cui-alert { width: auto; position: static; } 12 .txt { border: #cfcfcf 1px solid; margin: 10px 0; width: 80%; } 13 ul, li { padding: 0; margin: 0; } 14 .cui_calendar, .cui_week { list-style: none; } 15 .cui_calendar li, .cui_week li { float: left; width: 14%; overflow: hidden; padding: 4px 0; text-align: center; } 16 </style> 17 </head> 18 <body> 19 <article class="containerCalendar"> 20 </article> 21 <div style="border: 1px solid black; margin: 10px; padding: 10px; clear: both;"> 22 華麗的分割</div> 23 <article class="containerAlert"> 24 </article> 25 <input type="text" id="addmsg" class="txt"> 26 <button id="addbtn" class="btn"> 27 show message</button> 28 <script type="text/underscore-template" id="template-alert"> 29 <div class=" cui-alert" > 30 <div class="cui-pop-box"> 31 <div class="cui-bd"> 32 <p class="cui-error-tips"><%=content%></p> 33 <div class="cui-roller-btns"> 34 <div class="cui-flexbd cui-btns-cancel"><%=cancel%></div> 35 <div class="cui-flexbd cui-btns-sure"><%=confirm%></div> 36 </div> 37 </div> 38 </div> 39 </div> 40 </script> 41 <script type="text/template" id="template-calendar"> 42 <ul class="cui_week"> 43 <% var i = 0, day = 0; %> 44 <%for(day = 0; day < 7; day++) { %> 45 <li> 46 <%=weekDayItemTmpt[day] %></li> 47 <%} %> 48 </ul> 49 50 <ul class="cui_calendar"> 51 <% for(i = 0; i < beginWeek; i++) { %> 52 <li class="cui_invalid"></li> 53 <% } %> 54 <% for(i = 0; i < days; i++) { %> 55 <% day = i + 1; %> 56 <% if (compaign && compaign.days && compaign.sign && _.contains(compaign.days, day)) { day = compaign.sign } %> 57 <li class="cui_calendar_item" data-date="<%=year%>-<%=month + 1%>-<%=day%>"><%=day %></li> 58 <% } %> 59 </ul> 60 </script> 61 <script type="text/javascript" src="../../vendor/underscore-min.js"></script> 62 <script type="text/javascript" src="../../vendor/zepto.min.js"></script> 63 <script src="../../src/underscore.extend.js" type="text/javascript"></script> 64 <script src="../../src/util.js" type="text/javascript"></script> 65 <script src="../../src/message-center-wl.js" type="text/javascript"></script> 66 <script src="../../src/mvc.js" type="text/javascript"></script> 67 <script type="text/javascript"> 68 69 (function () { 70 var htmltemplate = $('#template-alert').html(); 71 72 var AlertView = _.inherit(Dalmatian.View, { 73 templateSet: { 74 0: htmltemplate 75 }, 76 77 statusSet: { 78 STATUS_INIT: 0 79 } 80 }); 81 82 83 var Adapter = _.inherit(Dalmatian.Adapter, { 84 parse: function (data) { 85 return data; 86 } 87 }); 88 89 var Controller = _.inherit(Dalmatian.ViewController, { 90 //設置默認信息 91 _initialize: function () { 92 this.origindata = { 93 content: '', 94 confirm: '肯定', 95 cancel: '取消' 96 } 97 }, 98 99 initialize: function ($super, opts) { 100 this._initialize(); 101 $super(opts); 102 this._init(); 103 }, 104 105 //基礎數據處理 106 _init: function () { 107 this.adapter.format(this.origindata); 108 this.adapter.registerObserver(this); 109 this.viewstatus = this.view.statusSet.STATUS_INIT; 110 }, 111 112 render: function () { 113 var data = this.adapter.viewmodel; 114 this.view.render(this.viewstatus, data); 115 }, 116 117 set: function (options) { 118 _.extend(this.adapter.datamodel, options); 119 // this.adapter.datamodel.content = options.content; 120 this.adapter.notifyDataChanged(); 121 122 //***************************** 123 //淳敏看這樣是否合理 124 //***************************** 125 if (options.content) { 126 Dalmatian.MSG.dispatch('ui', 'alertShow', options.content); 127 } 128 129 }, 130 131 events: { 132 "click .cui-btns-cancel": "cancelaction" 133 }, 134 135 cancelaction: function () { 136 this.onCancelBtnClick(); 137 } 138 }); 139 140 var view = new AlertView() 141 var adapter = new Adapter(); 142 143 var controller = new Controller({ 144 view: view, 145 adapter: adapter, 146 container: '.containerAlert', 147 onCancelBtnClick: function () { 148 alert('cancel 2') 149 } 150 }); 151 152 $('#addbtn').on('click', function (e) { 153 var content = $('#addmsg').val(); 154 // adapter.datamodel.content = content; 155 // adapter.notifyDataChanged(); 156 controller.set({ content: content, confirm: '肯定1' }); 157 controller.show(); 158 }); 159 160 })(); 161 162 (function () { 163 164 var tmpt = $('#template-calendar').html(); 165 166 var CalendarView = _.inherit(Dalmatian.View, { 167 templateSet: { 168 0: tmpt 169 }, 170 171 statusSet: { 172 STATUS_INIT: 0 173 } 174 }); 175 176 var CalendarAdapter = _.inherit(Dalmatian.Adapter, { 177 _initialize: function ($super) { 178 $super(); 179 180 //默認顯示方案,能夠根據參數修改 181 //任意一個model發生改變皆會引發update 182 this.weekDayItemTmpt = ['日', '一', '二', '三', '四', '五', '六']; 183 }, 184 185 //該次從新,viewmodel的數據徹底來源與parse中多定義 186 parse: function (data) { 187 return _.extend({ 188 weekDayItemTmpt: this.weekDayItemTmpt 189 }, data); 190 } 191 }); 192 193 var CalendarController = _.inherit(Dalmatian.ViewController, { 194 195 _initialize: function () { 196 this.view = new CalendarView(); 197 this.adapter = new CalendarAdapter(); 198 199 //默認業務數據 200 this.dateObj = new Date(); 201 this.container = '.containerCalendar'; 202 203 var s = ''; 204 }, 205 206 initialize: function ($super, opts) { 207 this._initialize(); 208 $super(opts); 209 210 //事件註冊點,應該單獨封裝 211 Dalmatian.MSG.register('ui', 'alertShow', $.proxy(function (data) { 212 var date = new Date(data); 213 this.handleDay(date, function (el) { 214 //***************** 215 //此處該作datamodel操做,暫時不予處理 216 //***************** 217 el.css('color', 'red'); 218 }); 219 220 var s = ''; 221 222 }, this)); 223 224 }, 225 226 onViewBeforeCreate: function () { 227 228 //使用adpter以前必須註冊監聽以及格式化viewModel,此操做應該封裝起來 229 this.adapter.registerObserver(this); 230 this.adapter.format(this._getMonthData(this.dateObj.getFullYear(), this.dateObj.getMonth())); 231 232 //view顯示以前一定會給予狀態,此應該封裝 233 this.viewstatus = this.view.statusSet.STATUS_INIT; 234 235 var s = ''; 236 }, 237 238 render: function () { 239 240 //該操做可封裝 241 var data = this.adapter.viewmodel; 242 243 console.log(data) 244 245 this.view.render(this.viewstatus, data); 246 }, 247 248 //根據傳入年月,返回該月相關數據 249 _getMonthData: function (year, month) { 250 this.date = new Date(year, month); 251 var d = new Date(year, month); 252 //description 獲取天數 253 var days = dateUtil.getDaysOfMonth(d); 254 //description 獲取那個月第一天時星期幾 255 var _beginWeek = dateUtil.getBeginDayOfMouth(d); 256 return { 257 year: d.getFullYear(), 258 month: d.getMonth(), 259 beginWeek: _beginWeek, 260 days: days, 261 compaign: null 262 }; 263 }, 264 265 266 handleDay: function (dateStr, fn) { 267 if (dateUtil.isDate(dateStr)) dateStr = dateUtil.format(dateStr, 'Y-m-d'); 268 var el = this.viewcontent.find('[data-date="' + dateStr + '"]'); 269 270 if (typeof fn == 'function') fn(el, dateUtil.parse(dateStr, 'y-m-d'), this); 271 272 } 273 274 }); 275 276 var calendar = new CalendarController(); 277 calendar.show(); 278 279 calendar.handleDay(new Date(), function (el, date, calendar) { 280 el.html('今天'); 281 }); 282 283 })(); 284 285 </script> 286 </body> 287 </html>
今天暫時到此,咱們下次繼續,本人技術有限,如果文中有任何不足以及問題,請提出
這裏命名空間以及id皆有可能像滾雪球似的越滾越多,因此這裏須要框架自己作出約定,具體看後期實踐吧......