在单页应用中,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为例,他在构造函数中定义了默认的属性:
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]
新闻热点
疑难解答