但是这个做法只能在单纯的回调函数模式里实现,在发布-订阅模式下是行不通的,因为回调函数只会跟一个函数引用相关,而发布-订阅模式里,同一个消息可能有多个订阅,如果把这种做法应用到发布-订阅里面,当调用this.trigger('beforeRender')的时候,会把跟beforeRender关联的所有订阅全部调用一次,那么以哪个订阅的返回值为准呢?也许你会说可以用队列中的最后一个订阅的返回值为准,在大多数情况下也许这么干没问题,但是当我们把“以队列最后的一个订阅返回值作为判断标准”这个逻辑加入到EventBase中的时候,会出现一个很大的风险,就是外部在使用的时候,一定得清楚地管理好订阅的顺序,一定要把那个跟校验等一些特殊逻辑相关的订阅放在最后面才行,而这种跟语法、编译没有关系,对编码顺序有要求的开发方式会给软件带来比较大的安全隐患,谁能保证任何时候任何场景都能控制好订阅的顺序呢,更何况公司里面可能还有些后来的新人,压根不知道你写的东西还有这样的限制。
解决这个问题的完美方式,就是像DOM对象的事件那样,在消息发布的时候,不是简简单单的发布一个消息字符串,而是把这个消息封装成一个对象,这个对象会传递给它所有的订阅,哪个订阅里觉得应该阻止这个消息发布之后的逻辑,只要调用这个消息的preventDefault()方法,然后在外部发布完消息后,调用消息的isDefaultPrevented()方法判断一下即可:
而这个做法跟使用jquery管理DOM对象的事件是一样的思路,比如bootstrap的大部分组件以及我在前面一些博客中写的组件都是用的这个方法来增加额外的判断逻辑,比如bootstrap的alert组件在close方法执行的时候有一段这样的判断:
按照这个思路去改造EventBase是一个解决问题的方法,但是jquery的一个小技巧,能够让我们把整个普通对象的事件管理变得更加简单,下面就让我们来瞧一瞧它的庐山真面目。
2. jquery小技巧模式
1)技巧一
如果在定义组件的时候,这个组件是跟DOM对象有关联的,比如下面这种形式:
那么我们可以完全给这个组件添加on off trigger one这几个常用事件管理的方法,然后将这些方法代理到$element的相应方法上:
通过代理,当调用组件的on方法时,其实调用的是$element的on方法,这样的话这种类型的组件就能支持完美的事件管理了。
2)技巧二
第一个技巧只能适用于跟DOM有关联的组件,对于那些跟DOM完全没有关联的组件该怎么添加像前面这样完美的事件管理机制呢?其实方法也很简单,只是我自己以前真的是没这么用过,所以这一次用起来才会觉得特别新鲜:
看截图中框起来的部分,只要给jquery的构造函数传递一个空对象,它就会返回一个完美支持事件管理的jquery对象。而且除了事件管理的功能外,由于它是一个jquery对象。所以jquery原型上的所有方法它都能调用,将来要是需要借用jquery其它的跟DOM无关的方法,说不定也能参考这个小技巧来实现。
3. 完美的事件管理实现
考虑到第2部分介绍的2种方式里面有重复的逻辑代码,如果把它们结合起来的话,就可以适用所有的开发组件的场景,也就能达到本文标题和开篇提到的让任意对象支持事件管理功能的目标了,所以最后结合前面两个技巧,把EventBase改造如下(是不是够简单):
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('./class'); /** * 这个基类可以让普通的类具备jquery对象的事件管理能力 */ var EventBase = Class({ instanceMembers: { init: function (_jqObject) { this._jqObject = _jqObject && _jqObject instanceof $ && _jqObject || $({}); }, on: function(){ return $.fn.on.apply(this._jqObject, arguments); }, one: function(){ return $.fn.one.apply(this._jqObject, arguments); }, off: function(){ return $.fn.off.apply(this._jqObject, arguments); }, trigger: function(){ return $.fn.trigger.apply(this._jqObject, arguments); } } }); return EventBase; });实际调用测试如下
1)模拟跟DOM关联的组件
测试代码一:
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('mod/class'); var EventBase = require('mod/eventBase'); var Demo = window.demo = Class({ instanceMembers: { init: function (element,options) { this.$element = $(element); this.base(this.$element); //添加监听 this.on('beforeRender', $.proxy(options.onBeforeRender, this)); this.on('render', $.proxy(options.onRender, this)); }, render: function () { //触发beforeRender事件 var e = $.Event('beforeRender'); this.trigger(e); if(e.isDefaultPrevented())return; //主要逻辑代码 console.log('render complete!'); //触发render事件 this.trigger('render'); } }, extend: EventBase }); var demo = new Demo('#demo', { onBeforeRender: function(e) { console.log('beforeRender event triggered!'); }, onRender: function(e) { console.log('render event triggered!'); } }); demo.render(); });