下面通过对组件的事件对象和事件机制两个方面分别对源码进行分析。
ExtJs组件事件对象是通过Ext.util.Event类来完成的,其实在浏览器元素事件中部分功能的实现已用到了该类。下面看该类的实现代码
EXTUTIL.Event = function(obj, name){ this.name = name;//事件名 this.obj = obj;//作用域 this.listeners = [];//多个监听函数的集合 };
这里说明一点,ExtJs为了实现更高的压缩,把Ext.util定义为常量EXTUTIL来处理。
该类是个集合类,它保存了多个监听函数,同时还需要和某个事件名对应起来。该类还实现了存放、删除、查找监听函数等相关操作。下面看具体的实现方法。
addListener : function(fn, scope, options){ var me = this, l; scope = scope || me.obj; if(!me.isListening(fn, scope)){//判断fn参数是否已经有同作用域的函数存在 l = me.createListener(fn, scope, options);//集合并不是保存监听函数,而是存放该函数的相关信息 // if we are currently firing this event, don't disturb the listener loop // 如果当前事件正在执行,为了不改变该事件的监听函数集合,重新复制当前集合,然后赋给listeners // 并利用me.listeners.push(l)加入新的监听函数,而原来的集合中正在被执行的代码在使用完成之后会垃圾回收,下一次执行的是新的集合,注意这种方式的实现 if(me.firing){ me.listeners = me.listeners.slice(0);//实现了复制功能 } me.listeners.push(l); } },
存放监听函数,首先判断fn参数是否已经有相同作用域的函数存在,如果存在则不会重复增加。l = me.createListener(fn, scope, options);代码返回的值并不是集合要保存的监听函数,而是存放该函数的相关信息,格式为:
this.listeners = [{fn : fn, scope : scope, options : o, fireFn: h}]
下面看createListener是怎么包裹监听函数的。
createListener: function(fn, scope, o){ o = o || {}; scope = scope || this.obj; var l = { fn: fn, scope: scope, options: o }, h = fn; if(o.target){ h = createTargeted(h, o, scope); } if(o.delay){ h = createDelayed(h, o, l, scope);//延迟运行 } if(o.single){ h = createSingle(h, this, fn, scope);//该监听函数只运行一次 } if(o.buffer){ h = createBuffered(h, o, l, scope);//缓存运行 } l.fireFn = h; return l; },
该函数中关于delay和buffer的区别,将在后续(定时器的讲解)给出答案。
创建完监听函数对象后,我们来看看他是如何执行的以及多个注册的监听函数执行顺序。ExtJs是通过Ext.util.fire来执行的,代码如下
fire : function(){ var me = this, listeners = me.listeners, len = listeners.length, i = 0, l; if(len > 0){ me.firing = TRUE;//表明正在执行 var args = Array.prototype.slice.call(arguments, 0); for (; i < len; i++) { l = listeners[i]; if(l && l.fireFn.apply(l.scope || me.obj || window, args) === FALSE) { return (me.firing = FALSE); } } } me.firing = FALSE; return TRUE; }
该函数通过传入的参数,来确定要执行的事件名(监听函数)
另一个比较重要的方法就是删除监听函数,代码如下
/** * 删除监听函数(事件) */ removeListener : function(fn, scope){ var index, l, k, me = this, ret = FALSE; if((index = me.findListener(fn, scope)) != -1){ if (me.firing) { me.listeners = me.listeners.slice(0); } l = me.listeners[index]; if(l.task) { l.task.cancel(); delete l.task; } k = l.tasks && l.tasks.length; if(k) { while(k--) { l.tasks[k].cancel(); } delete l.tasks; } me.listeners.splice(index, 1);//删除 ret = TRUE; } return ret; },
而方法clearListeners为删除所有监听函数
clearListeners : function(){ var me = this, l = me.listeners, i = l.length; while(i--) { me.removeListener(l[i].fn, l[i].scope); } },
组件事件机制是采用观察者模式实现的,即一部分由类库来实现(观察者),另一部分由开发者来实现(被观察者)。ExtJs组件事件机制是在Ext.util.Observable类中实现的,只要继承了该类就拥有了事件处理的能力。首先来看代码中的观察者,观察者用来观测开发者是否实现了该接口,如被观察者实现了该接口就会执行该接口对应的功能,而定义该接口是由观察者来编写的。在Ext.util.Observable中,观察者是由函数addEvent来实现的。
addEvents : function(o){ var me = this; me.events = me.events || {}; if (typeof o == 'string') { var a = arguments, i = a.length; while(i--) { me.events[a[i]] = me.events[a[i]] || TRUE; } } else { Ext.applyIf(me.events, o);//事件名及其对应的事件对象都存放在events中 o应该为{'eventName':true}格式 } },
该函数有两种调用形式
this.addEvents({"fired" : true, "quit" : onQuit});// onQuit为函数
this.addEvents("fired","quit");
该函数只在events中保存了事件名,这样做的好处是减少内存的占有,提高效率,在addListener才会正在的添加监听函数。运行监听函数是通过fireEvent函数来实现的,代码如下
fireEvent : function(){ var a = Array.prototype.slice.call(arguments, 0), ename = a[0].toLowerCase(),//事件名 me = this, ret = TRUE, ce = me.events[ename], cc, q, c; if (me.eventsSuspended === TRUE) {//支持组件的事件挂起 if (q = me.eventQueue) { q.push(a); } } else if(typeof ce == 'object') { if (ce.bubble){//是否能进行冒泡处理 if(ce.fire.apply(ce, a.slice(1)) === FALSE) {//触发该事件,调用Ext.util.Event.fire,返回false结束下面的操作 return FALSE; } c = me.getBubbleTarget && me.getBubbleTarget();//进行冒泡的元素 if(c && c.enableBubble) { cc = c.events[ename]; if(!cc || typeof cc != 'object' || !cc.bubble) { c.enableBubble(ename); } return c.fireEvent.apply(c, a); } } else { a.shift(); ret = ce.fire.apply(ce, a); } } return ret; },
该函数第一个参数是用来指定事件名,之后的n个参数将用作监听函数的参数,在调用该方法时应该至少要有事件的名称,方法中判断了是否进行冒泡处理。该方法的调用如下
this.fireEvent(‘disable’,this);//Ext.Component类中的disable事件。
在实际开发中,要想使组件或类拥有事件能力,首先需要继承Ext.util.Observable,然后调用上面的方法给它注册事件名和建立监测点。接下来看被观察者
被观察者就是开发者编写的事件监听函数,并把他注册到相对应的事件中,让组件监测它。这个注册动作是由方法addListener函数来完成的,代码如下:
addListener : function(eventName, fn, scope, o){ var me = this, e, oe, ce; //采用紧缩形式的参数对象形式 /* * 例如 myGridPanel.on({ 'click' : this.onClick, 'mouseover' : this.onMouseOver, 'mouseout' : this.onMouseOut, scope: this }); */ if (typeof eventName == 'object') { o = eventName; for (e in o) { oe = o[e]; if (!me.filterOptRe.test(e)) { //私有配置项和共享配置项注册事件 me.addListener(e, oe.fn || oe, oe.scope || o.scope, oe.fn ? oe : o); } } } else { eventName = eventName.toLowerCase(); ce = me.events[eventName] || TRUE; //如果没有调用addEvents,即addEvents中没有生成事件对象,推迟到这里实现,所以即使在观察者时没有调用addEvents对该事件进行注册也可以 if (typeof ce == 'boolean') { me.events[eventName] = ce = new EXTUTIL.Event(me, eventName); } ce.addListener(fn, scope, typeof o == 'object' ? o : {}); } },
此处实现与元素事件(浏览器事件的注册)类似,不同之处这里的配置项只有四种:scope|delay|buffer|single
下面看删除事件方法
removeListener : function(eventName, fn, scope){ var ce = this.events[eventName.toLowerCase()]; if (typeof ce == 'object') { ce.removeListener(fn, scope); } },
该方法中调用Ext.util.Event.removeListener来删除已注册的事件
删除所有的监听事件,代码如下
purgeListeners : function(){ var events = this.events, evt, key; for(key in events){ evt = events[key]; if(typeof evt == 'object'){ evt.clearListeners(); } } },
ExtJs还提供了基于拦截的事件实现,它采用拦截思想(AOP)。具体来说可以采用Ext.util.Observable类的beforeMethod和afterMethod方法来实现拦截的功能。代码如下
// adds an 'interceptor' called before the original method beforeMethod : function(method, fn, scope){ getMethodEvent.call(this, method).before.push({ fn: fn, scope: scope }); }, // adds a 'sequence' called after the original method afterMethod : function(method, fn, scope){ getMethodEvent.call(this, method).after.push({ fn: fn, scope: scope }); },
这两个方法都统一调用了getMethodEvent方法,代码如下
function getMethodEvent(method){ var e = (this.methodEvents = this.methodEvents || {})[method], //用来保存事件对象 returnValue, v, cancel, obj = this; if (!e) {//该参数中传入的函数尚未注册 this.methodEvents[method] = e = {}; e.originalFn = this[method]; e.methodName = method; e.before = []; e.after = []; var makeCall = function(fn, scope, args){//用来执行before和after集合的事件监听函数 if((v = fn.apply(scope || obj, args)) !== undefined){//运行监听函数 if (typeof v == 'object') {//如果返回值是对象时,取得对象中的returnValue或返回值 if(v.returnValue !== undefined){ returnValue = v.returnValue; }else{ returnValue = v; } cancel = !!v.cancel;//如果该返回值指明cancel为true,那么就不执行接下来的函数 } else if (v === false) { cancel = true; } else { returnValue = v; } } }; this[method] = function(){//把传入的函数名指定的函数封装成执行的函数序列函数 var args = Array.prototype.slice.call(arguments, 0), b; returnValue = v = undefined; cancel = false; //执行before事件处理函数 for(var i = 0, len = e.before.length; i < len; i++){ b = e.before[i]; makeCall(b.fn, b.scope, args); if (cancel) { return returnValue; } } //执行传入的函数名指定的函数 if((v = e.originalFn.apply(obj, args)) !== undefined){ returnValue = v; } //执行after事件处理函数 for(var i = 0, len = e.after.length; i < len; i++){ b = e.after[i]; makeCall(b.fn, b.scope, args); if (cancel) { return returnValue; } } return returnValue; }; } return e; }
当调用beforeMethod或afterMethod方法,它就通过了getMethodEvent方法生成一个与它指定的拦截的函数名对应的方法事件对象。这个对象中的有before和after两个集合属性。调用beforeMethod方法就会向这个before集合加入事件的监听方法。调用afterMethod方法则向after集合中添加。
当调用该方法后,会在methodEvents集合中保存事件对象,这个对象中事件名就是拦截的方法名,它的格式如下:
{originalFn:onselect,methodName:’onselect’,before:[],after:[]}。
其中originalFn是指拦截的方法,methodName就是拦截的方法名。接着的before和after就是用来保存该拦截方法的之前或之后执行的函数。
其中makeCall是用来执行before和after集合的监听函数,程序中
//执行before事件处理函数
for(var i = 0, len = e.before.length; i < len; i++){
b = e.before[i];
makeCall(b.fn, b.scope, args);
if (cancel) {
return returnValue;
}
}
和
//执行after事件处理函数
for(var i = 0, len = e.after.length; i < len; i++){
b = e.after[i];
makeCall(b.fn, b.scope, args);
if (cancel) {
return returnValue;
}
}
通过循环来分别运行它们各自己集合的监听函数。而此处
//执行传入的函数名指定的函数
if((v = e.originalFn.apply(obj, args)) !== undefined){
returnValue = v;
}
是运行被拦截的方法。看出来,makeCall是包裹函数,它的包裹的主要作用就是处理返回值。
对于返回值,getMethodEvent建立了两个变量returnValue、cancel用来保存每次运行时返回的状态,每次运行makeCall都会设定(改变或保持)其值。returnValue是保存其返回值,而cancel的意思是在运行完这个函数之后是否立马中断,不执行该函数链中其它函数。如果cancel为true时中断执行,实现拦截的效果。
看下面的例子
Ext.onReady(function() { Ext.BLANK_IMAGE_URL = '../ext/resources/images/default/s.gif'; Ext.QuickTips.init(); // 事件拦击器的实现 var Interceptor = Ext.extend(Ext.util.Observable, { constructor : function(config) { Ext.apply(this, config); this.addEvents('select');//添加观察者事件 this.beforeMethod('onSelect', this.beforeSelectFn, this);//拦截器 this.afterMethod('onSelect', this.afterSelectFn, this); Interceptor.superclass.constructor.call(this); }, fireInterceptorEvent : function() { this.fireEvent('select', this.onSelect, 'arg1', 'arg2'); }, beforeSelectFn : function() { alert('before'); return { returnValue : 'interceptor', cancel : true } }, afterSelectFn : function() { alert('after'); } }); var example = new Interceptor({ onSelect : function() { var a = Array.prototype.slice.call(arguments, 1); Ext.each(a, function(item, index, obj) { alert(item); }); } }); //被观察者 example.addListener('select', example.onSelect, this); var buttonEl = Ext.get('btn'); buttonEl.on('click', function(e, t, o) { example.fireInterceptorEvent(); }, this); });
运行该代码时,首先添加观察者事件和拦截对象,内存中的快照(部分)为:
Ext.util.Observable中
this.methodEvents = {onSelect:{originalFn : onSelect,methodName:'onSelect',after :[{scope:scope,fn:afterSelectFn}],before :[{scope:scope,fn:beforeSelectFn}]}}
在实例化Interceptor对象后,添加被观察者时Ext.util.Observable.addListener中会初始化
this.events[eventName] = ce = new EXTUTIL.Event(this, eventName);,此时会把Ext.util.Observable对象传入Ext.util.Event,当然包括属性methodEvents。当点击按钮时,会调用Ext.util.Observable.fireEvent,而该方法中又会调用EXTUTIL.Event.fire执行firefn函数,即beforeSelectFn,见代码
if(l && l.fireFn.apply(l.scope || me.obj || window, args) === FALSE) {
return (me.firing = FALSE);
}
以上详细的介绍了类Ext.util.Observable。Ext强大的事件功能远不至此,它还提供了与事件相关的快捷键、导航键和鼠标按键事件等。且听下回分解。