前言
在單頁應用中,view與view之間的通信機制一直是一個重點,因為單頁應用的所有操作以及狀态管理全部發生在一個頁面上
沒有很好的組織的話很容易就亂了,就算表面上看起來沒有問題,事實上會有各種隐憂,各種坑等着你去跳
最初就沒有一定理論上的支撐,極有可能是這麼一種情況:
① 需求下來了,搞一個demo做交待
② 發現基本滿足很滿意,于是直接在demo中做調整
上面的做法本身沒有什麼問題,問題發生在後期
③ 在demo調整後應用到了實際業務中,發現很多地方有問題,于是見一個坑解決一個坑
④ 到最後感覺整個架構零零散散,有很多if代碼,有很多代碼不太懂意思,但是一旦移除就報錯
這個時候我們就想到了重構,重構過程中就會發現最初的設計,或者說整個架構的基礎有問題,于是就提出推翻重來
若是時間上允許,還可以,但是往往重構過程中,會多一些不按套路出牌的同學,将API接口給換了,這一換所有的業務系統全部崩潰
是以說,新的架構會對業務線造成壓力,會提高測試與編碼成本,于是就回到了我們上篇部落格的問題
【UI插件】簡單的月曆插件(下)—— 學習MVC思想
一些同學認為,以這種方式寫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中的消息機制
簡單而言,以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 });
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與calendar之間的通信
我們實作這樣的效果,點選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();
6 if (options.content) {
7 Dalmatian.MSG.dispatch('ui', 'alertShow', options.content);
8 }
10 },
于是便有了這樣的效果,每次設定值的時候,我這裡都會被觸發
而且這裡的this指向的是calendar,是以我們這裡可以做處理,由于時間原因,我這裡便亂幹了
可以看到,每次操作後,calendar得到了更新,但是由于我這裡是直接操作的dom未做datamodel操作,是以沒有做狀态儲存,第二次實際上是該重新整理的
這裡我們暫時不管
View Code
結語
今天暫時到此,我們下次繼續,本人技術有限,若是文中有任何不足以及問題,請提出
這裡命名空間以及id皆有可能像滾雪球似的越滾越多,是以這裡需要架構本身做出約定,具體看後期實踐吧......
本文轉自葉小钗部落格園部落格,原文連結:http://www.cnblogs.com/yexiaochai/p/3721802.html,如需轉載請自行聯系原作者