从去年起项目中开始使用Extjs,在熟悉Ext API以及各组件的用法之余也抽空看了Ext源码,但40000多行的代码在时间和理解上都有很大困难,所以就专门了解了其中某些部分的代码。尤其针对Ext.Observble接口源代码进行了反复阅读,对Ext关于事件处理机制上有了一些初步的了解。

结合之前刚刚阅读的道格拉斯先生所著的《JavaScript:The Good Parts》中关于接口继承的函数化方式,自己尝试的写了一套简单的Javascript事件处理管理器。当然在正常的开发过程中还是建议使用成熟JS框架,这里所讨论的内容仅仅是加强大家对JS事件机制的理解以及观察者模式的应用。

OK 上代码之前我先说说Extjs中事件管理实现机制。

一:Extjs事件管理器分为两个模块,其中之一就是UI开发中必不可少的DOM事件,Extjs采用DOM LEVEL2 事件处理模型,关于它再此就不多加阐述了。

if(window.addEventListener){
target.addEventListener(eventname, handler, false);//布尔值是Capture属性,决定监听函数是在冒泡时还是捕获时执行,默认传入false冒泡执行
}
else if (window.attachEvent){
target.attachEvent(
'on' + eventname, handler);
}

上面的代码想必大家都知道,这是一个对IE以及其他标准W3C事件模型的兼容性代码。IE在执行效果上有区别,即函数内作用域指向window....... 

Extjs使用Ext.EventManager进行DOM事件管理,功能很强大!!但是代码非常的繁琐复杂........大致设计结构就是再调用W3C标准事件方法之前,对监听函数进行处理:诸如封装作用域,执行调用延迟或缓存加载等,并封装了标准Event对象再传递给监听函数,Ext的DOM事件模块中没有实现Event fire的功能,但是Prototype类库中有相应代码。

二:Ext事件管理器还引入了一个全新的概念:软件驱动事件模型。Ext.Observable接口能给Ext所有组件或类增加自定义事件的功能,这已经脱离DOM层面,属于流程控制上触发事件。实现接口的对象在初始化过程中注册自定义事件,然后在内部代码执行过程中触发各种事件,可以对流程进行控制,使用对象时就可以和DOM事件一样注册每个事件的监听函数,就像DOM一样自然。几乎所有的Ext UI组件和类都是DOM标准事件和软件驱动事件结合使用,这也使得Ext的UI组件相当的灵活多变,我计划在一下篇文章中谈论Ext UI组件中广泛使用的事件代理模式,并制作一个简单的Grid UI来进行说明。

好了说了半天进入正篇吧,自己写一套具备上述功能的事件模型是完全可行的,而且也不复杂。我设计的事件管理器也仿照Ext分成两个方面:DOM-Base Event 和 Software-Driven Event。先来看看DOM事件:

 

// 命名空间 HL
if(HL === undefined) var HL = {};
/**
* 事件处理功能函数,用于操作DOM对象的事件监听
*/
HL.EventManager = function(){

// 事件管理器储存DOM元素的缓存,存放监听函数列表
// 格式存储为 elCache.domid.eventname
var elCache = {},

// 处理浏览器兼容性的代码,该代码只在页面加载时运行一次
addEventHandler = function(){
var h;
if(window.addEventListener){
h
= function(target, eventname, handler){
target.addEventListener(eventname, handler,
false);
};
}
else if (window.attachEvent){
h
= function(target, eventname, handler){
target.attachEvent(
'on' + eventname, handler);
};
}
return h;
}(),
removeEventHandler
= function(){
var h;
if(window.removeEventListener){
h
= function(target, eventname, handler){
target.removeEventListener(eventname, handler,
false);
};
}
else if (window.detachEvent){
h
= function(target, eventname, handler){
target.detachEvent(
'on' + eventname, handler);
};
}
return h;
}();


return {

// 给DOM元素添加事件监听函数
addListener : function(target, eventname, handler, scope){
var el = typeof target === 'object' ? target : document.getElementById(target),
id;
// 封装标准Event对象,并改变监听函数的作用域
wrap = function(event){
var e = HL.lib.Event(e); //event || window.event;
handler.call(scope || el, e);
};

if(!el)
throw 'The element is not exist in the document!';
id
= el.id;
if(!elCache[id]){
elCache[id]
= {};
}
if(!elCache[id][eventname]){
elCache[id][eventname]
= [];
}
// 将监听函数存入缓存
elCache[id][eventname].push({
fn : handler,
wrap : wrap
});
// 添加监听函数,该方法在闭包中,Wrap为DOM事件实际触发的函数
addEventHandler(el, eventname, wrap);
return el;
},

// 删除DOM元素事件监听函数
removeListener : function(target, eventname, handler){
var el = typeof target === 'object' ? target : document.getElementById(target),
id, listeners, wrap;

if(!el)
throw 'The element is not exist in the document!';
id
= el.id;
// 在缓存中查找监听函数,取出实际添加的经过封装的函数
if(elCache[id][eventname]){
listeners
= elCache[id][eventname];
for(var i = 0, len = listeners.length; i < len; i++){
if(listeners[i].fn === handler){
wrap
= listeners[i].wrap;
}
}
if(!wrap)
return el;
}
else {
return el;
}
// 删除监听函数,这里删除的是封装过的函数
removeEventHandler(el, eventname, wrap);
return el;
}
};
}();
// 简化函数名称,改变对外的接口
HL.on = HL.EventManager.addListener;
HL.un = HL.EventManager.removeListener;

简单描述一下代码:代码中用到了大量的闭包,尤其是闭包量函数 addEventHandler 和 removeEventHandler,function(){}() 的写法非常有用,定义完立刻执行,能保证浏览器兼容性代码只运行一次,这也算是一种小小的性能优化吧。

addListener 方法中有两个重要操作:闭包函数wrap以及缓存elCache。wrap闭包函数作用是:1)封装了标准Event对象,这里的HL.lib.Event代码没有给出,就是解决浏览器兼容性问题,向外界统一Event操作接口;2) 改变监听函数的作用域,这就解决了IE下函数作用域指向window的问题。 elCache缓存就是存放监听函数列表,因为添加的并不是指定的函数,而是一个封装过的wrap闭包函数,所以要保存起来保证 removeEventListener 的正常执行效果。

测试代码如下:

<body>
<div id="father">
<span id="test">test</span>
<span id="test2">test2</span>
</div>
<button id="remove">remove listeners</button>
</body>
<script type="text/javascript">
function eventTest(e){
var t = e.getTarget();//e.target,
cn = this.children;
for(var i = 0; i < cn.length; i++){
if(t === cn[i]) console.log(i);
}
}
function testScope(){
console.log(
this.text);
}
HL.on(
'father', 'click', eventTest);
HL.on(
'father', 'click', testScope, {text : 'scope!'});
HL.on(
'remove', 'click', function(){
HL.un(
'father', 'click', eventTest);
HL.un(
'father', 'click', testScope);
});
</script>

执行效果就是点击father div中的文本,控制台打印出各个Span的序列号 和 "scope!", 点击remove按钮后,点击div则无任何反应。

下面则是另一大重头戏:软件驱动事件模型

这是一种很奇妙的设计,使得一个普通的Javascript对象能够像DOM对象一样添加监听触发事件。Ext采用Observable接口来实现,观察者模式运用在这里最合适不过。最近刚好看完那本道格拉斯大牛的《Javascript语言精粹》,了解到一种函数化的继承机制,我在实现软件驱动事件时采用了这种继承方法,而设计原理则模仿Ext的Observer模式:

if(HL === undefined) var HL = {};
/**
* 事件处理接口 target参数是需要添加事件处理功能的任何一个对象
*/
HL.Observable
= function(target){

  
// 闭包中的观察者对象,储存该对象所有事件的监听器
  var observer = {};

  
// 为某一事件添加监听器
  target.on = function(event, listener, obj, config){
    
var l = observer[event],
 handler = listener,     
        config = config || {}, o;
args
= Array.prototype.slice.call(arguments, 3),

    if(config.single === true){// 监听事件只触发一次
       o = this;
       handler = function(){
        o.removeListener(event, listener, this);
        return listener.apply(this, arguments);
      };
    }
    var ct = {
      fn : listener, // 作为删除时检索的关键字
      fireFn : handler, // 触发时实际运行的函数
      scope : scope || this,// 防止作用域为空时时删除时不成功
      args : array
    };
// 将监听函数,作用域 参数 存入观察者缓存对象
if(l === undefined)
  observer[event]
= [ct];
else
  observer[event].push(ct);
return this;
};

// 触发某事件的所有监听器
target.fire = function(event){
var listeners = observer[event],
func,scope,paramters,
len
= listeners.length;
args
= Array.prototype.slice.call(arguments, 1); // 触发时传入的参数,传递给监听函数

if(listeners){ // 从观察者对象中读取对于事件的所有监听函数,顺序执行
for(var i = 0; i < len; i++){
func
= listeners[i].fn;
scope
= listeners[i].scope;
paramters
= listeners[i].args || [];
// 执行监听函数,如果返回false则中断触发,向调用栈上层返回false
if(func.apply(scope||this, paramters.concat(args)) === false)
return false;
}
}
return true;
};

// 移除事件某一个监听器
target.removeListener = function(event, fn, scope){
var l = observer[event],
i
= l.length-1;
scope
= scope || this;
while(i--){
if(l[i].fn === fn && l[i].scope === scope)
break;
}
l.splice(i,
1);// Javascript数组的原生方法
return true;
};

return target;
};

代码其实很简单,观察者Observer是一个存放该对象触发事件所有监听函数的集合,添加监听函数,然后在其他地方调用fire进行手工触发事件,这可能是Ext中没有DOM手动触发事件的一个原因吧,Ext的Observable接口功能很强大,通过传入的配置项参数能对监听函数做很精确的控制。在此值得注意的是闭包中的私有变量 Observer,HL.Observable每执行一次,都会生成一个单独的Observer变量,和传入的Target对象一一对应,这一点和HL.EventManager不同,关于闭包与作用域的概念,如果哪位同学不懂的话建议先去了解下。下面看看测试代码:

<body>
<div id="test">
<span>
hello world!!
</span>
</div>
</body>
<script type="text/javascript">

var testClass = function(domID){
var processEvent = function(e){
if(e.button == 2){ // 点击鼠标右键的时候,触发contextMenu事件的监听函数
this.fire('contextMenu');
}
else { // 触发click事件的监听函数
this.fire('click');
}
};
HL.Observable(
this); // 使该对象具备事件处理功能
HL.on(domID,
'mousedown', processEvent); // 给值得ID的dom对象添加鼠标点击监听事件
this.name = 'test';
}
var test = testClass('test'); //为Test对象注册contextMenu于click事件的监听函数
test.on(
'contextMenu', function(arg){
alert(
this.name + ' right');
},
test);
.on(
'click', function(arg){
alert(
this.name + ' left');
},
test, {single : true});

</script>

当在div中点击鼠标左键时,弹出"test left",点击右键时,弹出"test right"。注意新版方法中,test left 只触发一次,你第二次点击btn时将不会触发监听函数。这也是DOM事件和软件驱动事件结合使用的一种事件代理模式。被广泛运用于Ext各个UI组件中。

以上就是自己完成的一个简单的事件处理模块,功能比较简单没有成熟JS框架的那么复杂强大,但是基本上可以满足很多应用的需求,由此可见自己从零开始手把手的建立JS framework也并不是不可能。

好了这是我的第一篇文章,希望大家能多多批评多多支持,明天开始写类继承机制以及一些基本功能的实现。祝大家工作顺利学习进步哦!!!