ExtJs源码分析与学习—ExtJs事件机制(一)

前面讲了ExtJs核心代码以及扩展后,今天来说说ExtJs的事件机制,要想弄明白ExtJs的事件机制,就必须先知道浏览器的事件机制,这里给出了浏览器事件机制与自定义事件的实现 。  


     首先看源码 ext-base-event.js 关于浏览器本身事件的封装。代码中实现了各主要浏览器的兼容,以及对一些事件进行了扩展。该代码中首先定义了类Ext.lib.Event,该类(函数)是一个匿名函数自执行,执行后返回对象pub,pub赋值给Ext.lib.Event。

 

Js代码    收藏代码
  1. Ext.lib.Event = function() {  
  2.     var loadComplete = false,  
  3.         unloadListeners = {},//用来存放el的unload事件  
  4.     …  
  5.     var pub = {  
  6.   …  
  7. };  
  8.     return pub;  
  9. }();  

 既然该类是围绕pub来实现的,我们首先来看pub的定义,pub中定义了许多关于事件处理的方法

 

Js代码    收藏代码
  1. onAvailable : function(p_id, p_fn, p_obj, p_override) {  
  2.             onAvailStack.push({  
  3.                 id:         p_id,  
  4.                 fn:         p_fn,  
  5.                 obj:        p_obj,  
  6.                 override:   p_override,  
  7.                 checkReady: false });  
  8.   
  9.             retryCount = POLL_RETRYS;  
  10.             startInterval();  
  11.         },  

 该方法中调用了startInterval()和_tryPreloadAttach()。_tryPreloadAttach() 、 onAvailable() 、startInterval() 这三个函数的执行机制大致是这样的:在文档还没有加载完成之前,可以通过 onAvailable() 方法给某个对象注册某类事件的监听器, onAvailable() 方法会调用 startInterval() 方法来启动一个轮询来执行 _tryPreloadAttach() ,轮询的周期是 POLL_INTERVAL (默认是 20ms ),轮询次数是 POLL_RETRYS ( 200 次)(这么来看的话,这种依靠不断轮询来尝试文档是否已经加载完成的方法最长是 20ms*200=4 秒钟)。 _tryPreloadAttach() 方法里面会判断文档是不是已经加载完成,如果加载完成,执行注册的监听器,并把定时器清除掉。_tryPreloadAttach() 方法还有一个 tryAgain 标志用来说明是不是要进行再次尝试。

 

Js代码    收藏代码
  1. addListener: function(el, eventName, fn) {  
  2.             el = Ext.getDom(el);  
  3.             if (el && fn) {  
  4.                 if (eventName == UNLOAD) {  
  5.                     if (unloadListeners[el.id] === undefined) {  
  6.                         unloadListeners[el.id] = [];  
  7.                     }  
  8.                     unloadListeners[el.id].push([eventName, fn]);  
  9.                     return fn;  
  10.                 }  
  11.                 return doAdd(el, eventName, fn, false);  
  12.             }  
  13.             return false;  
  14.         },  

 该方法为元素添加注册事件,el为添加事件的元素,eventName为事件名称(如click),fn为响应函数(hanlder)。对“unload”事件做了单独处理,内部调用了私有的doAdd函数。

 

Js代码    收藏代码
  1. doAdd = function() {  
  2.             var ret;  
  3.             if (win.addEventListener) {//标准浏览器  
  4.                 ret = function(el, eventName, fn, capture) {  
  5.                     if (eventName == 'mouseenter') {  
  6.                         fn = fn.createInterceptor(checkRelatedTarget);  
  7.                         el.addEventListener(MOUSEOVER, fn, (capture));  
  8.                     } else if (eventName == 'mouseleave') {  
  9.                         fn = fn.createInterceptor(checkRelatedTarget);  
  10.                         el.addEventListener(MOUSEOUT, fn, (capture));  
  11.                     } else {  
  12.                         el.addEventListener(eventName, fn, (capture));  
  13.                     }  
  14.                     return fn;  
  15.                 };  
  16.             } else if (win.attachEvent) {//ie浏览器,ie9中会同时支持这两种方式win.attachEvent和win.addEventListener,  
  17.                 ret = function(el, eventName, fn, capture) {  
  18.                     el.attachEvent("on" + eventName, fn);  
  19.                     return fn;  
  20.                 };  
  21.             } else {  
  22.                 ret = function(){};  
  23.             }  
  24.             return ret;  
  25.         }(),  

 

该函数为闭包函数,即自执行函数,会返回ret对应的函数。并且添加了其他浏览器(IE除外)不支持的事件mouseenter和mouseleave ,为非IE浏览器间接实现了这两个事件,需要另两个函数的辅助。这两个辅助函数的实现也可以用到其他没有引入ExtJs的项目中。

 

Js代码    收藏代码
  1. function checkRelatedTarget(e) {  
  2.         return !elContains(e.currentTarget, pub.getRelatedTarget(e));  
  3. }  
  4. //判断某个元素child是否是parent的子元素,是则返回true,否则false。  
  5.     function elContains(parent, child) {  
  6.        if(parent && parent.firstChild){  
  7.          while(child) {  
  8.             if(child === parent) {  
  9.                 return true;  
  10.             }  
  11.             child = child.parentNode;  
  12.             //nodeType 属性返回被选节点的节点类型。等于1为节点Element,当child不是parent的孩子节点时,会一直执行,直到child.nodeType 为 9 document时  
  13.             if(child && (child.nodeType != 1)) {  
  14.                 child = null;  
  15.             }  
  16.           }  
  17.         }  
  18.         return false;  
  19. }  

 

elContains 两个参数parent,child判断某个元素child是否是parent的子元素,是则返回true,否则false。
checkRelatedTarget 会作为一个拦截器,这里e.currentTarget是指添加事件的元素本身。pub.getRelatedTarget(e)返回的是跟该事件相关的元素标准浏览器用relatedTarget IE中用fromElement,toElement。在Ext.lib.Dom中也有个实现判断父子元素的方法isAncestor,后续会讲到。不知道ExtJs为什么要写两个实现,个人推测这两段代码可能是两个人实现的,并且实现的原理有些不同,故保留了两个。

 

下面看getRelatedTarget的实现

 

Js代码    收藏代码
  1. getRelatedTarget : function(ev) {  
  2.             ev = ev.browserEvent || ev;  
  3.             return this.resolveTextNode(ev.relatedTarget ||  
  4.                 (/(mouseout|mouseleave)/.test(ev.type) ? ev.toElement :  
  5.                  /(mouseover|mouseenter)/.test(ev.type) ? ev.fromElement : null));  
  6.         },  

 

 

这里有几个浏览器事件对象属性需说明一下

target 指事件源对象,点击嵌套元素最里层的某元素,该元素就是target。IE6/7/8对应的是srcElement。

currentTarget 指添加事件handler的元素本身,如el.addEventListener中el就是currentTarget。IE6/7/8没有对应属性,可在handler内使用this来替代如evt.currentTarget = this。

relativeTarget 指事件相关的元素,一般用在mouseover,mouseout事件中。IE6/7/8中对应的是fromElement,toElement。

 

getRelatedTarget 方法返回相关的元素结点,在该方法中resolveTextNode辅助函数又判断了火狐和其他浏览器的不同。

 

Js代码    收藏代码
  1. resolveTextNode : Ext.isGecko ? function(node){  
  2.             if(!node){  
  3.                 return;  
  4.             }  
  5.             // work around firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=101197  
  6.             var s = HTMLElement.prototype.toString.call(node);  
  7.             if(s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]'){  
  8.                 return;  
  9.             }  
  10.             return node.nodeType == 3 ? node.parentNode : node;  
  11.         } : function(node){  
  12.             return node && node.nodeType == 3 ? node.parentNode : node;  
  13.         },  

 

下面看与addListener对应的方法removeListener

 

Js代码    收藏代码
  1. /** 
  2.   * 删除 el 事件 
  3.   */  
  4.  removeListener: function(el, eventName, fn) {  
  5.      el = Ext.getDom(el);  
  6.      var i, len, li, lis;  
  7.      if (el && fn) {  
  8.          if(eventName == UNLOAD){  
  9.              if((lis = unloadListeners[el.id]) !== undefined){  
  10.                  for(i = 0, len = lis.length; i < len; i++){  
  11.                      if((li = lis[i]) && li[TYPE] == eventName && li[FN] == fn){  
  12.                          unloadListeners[el.id].splice(i, 1);  
  13.                      }  
  14.                  }  
  15.              }  
  16.              return;  
  17.          }  
  18.          doRemove(el, eventName, fn, false);  
  19.      }  
  20.  },  

 

该方法与addListener对应,用来删除由addListener注册的事件,实际调用是利用Ext.EventManager中的方法来调用,该类对这两个方法又进行了更进一步的封装,后续待分析。该方法中用到了辅助函数doRemove

 

Js代码    收藏代码
  1. /** 
  2.   * 返回一个符合当前浏览器的函数用来注销事件, 
  3.      对于非ie下的浏览器也让其可以支持mouseleave/mouseenter 
  4.   */  
  5.  doRemove = function(){  
  6.      var ret;  
  7.      if (win.removeEventListener) {  
  8.          ret = function (el, eventName, fn, capture) {  
  9.              if (eventName == 'mouseenter') {  
  10.                  eventName = MOUSEOVER;  
  11.              } else if (eventName == 'mouseleave') {  
  12.                  eventName = MOUSEOUT;  
  13.              }  
  14.              el.removeEventListener(eventName, fn, (capture));  
  15.          };  
  16.      } else if (win.detachEvent) {  
  17.          ret = function (el, eventName, fn) {  
  18.              el.detachEvent("on" + eventName, fn);  
  19.          };  
  20.      } else {  
  21.          ret = function(){};  
  22.      }  
  23.      return ret;  
  24.  }();  
 

 

除了注册和删除事件,Ext还对其他原生的事件方法(属性)进行了封装,看下面

 

Js代码    收藏代码
  1. getTarget : function(ev) {  
  2.             ev = ev.browserEvent || ev;  
  3.             return this.resolveTextNode(ev.target || ev.srcElement);  
  4.         },  

 获取当前事件源对象

 

Js代码    收藏代码
  1. getPageX : function(ev) {  
  2.             return getPageCoord(ev, "X");  
  3.         },  
  4.   
  5.         getPageY : function(ev) {  
  6.             return getPageCoord(ev, "Y");  
  7.         },  
  8.   
  9.   
  10.         getXY : function(ev) {  
  11.             return [this.getPageX(ev), this.getPageY(ev)];  
  12.         },  

 这三个函数用来获取鼠标事件源的坐标(水平,垂直)坐标,其中调用了辅助函数getPageCoord和getScroll,该方法中解决了不同浏览器之间的不同,实现了兼容。

 

Js代码    收藏代码
  1. function getPageCoord (ev, xy) {  
  2.         ev = ev.browserEvent || ev;  
  3.         var coord  = ev['page' + xy];  
  4.         if (!coord && coord !== 0) {  
  5.             coord = ev['client' + xy] || 0;  
  6.   
  7.             if (Ext.isIE) {  
  8.                 coord += getScroll()[xy == "X" ? 0 : 1];  
  9.             }  
  10.         }  
  11.   
  12.         return coord;  
  13. }  

私有的getPageCoord方法用来获取鼠标事件时相对于文档的坐标(水平,垂直)。

Firefox引入了pageX   /  Y   ,IE9/Safari/Chrome/Opera虽然支持但仅在文档(document)内而非页面(page)。

Safari/Chrome/Opera可以使用标准的clientX/Y获取,IE下可通过clientX/Y与scrollLeft/scrollTop计算得到。

 

 

 

Js代码    收藏代码
  1. function getScroll() {  
  2.         var dd = doc.documentElement,  
  3.             db = doc.body;  
  4.         if(dd && (dd[SCROLLTOP] || dd[SCROLLLEFT])){  
  5.             return [dd[SCROLLLEFT], dd[SCROLLTOP]];  
  6.         }else if(db){  
  7.             return [db[SCROLLLEFT], db[SCROLLTOP]];  
  8.         }else{  
  9.             return [0, 0];  
  10.         }  
  11.     }  

 私有的getScroll方法返回文档的scrollTop和scrollLeft值,由于浏览器差异,该实现上先从document.documentElement取,为0后再从document.body上取。都没有的话返回[0,0]。

 

 

下面看另外三个方法

 

Js代码    收藏代码
  1. stopEvent : function(ev) {  
  2.      this.stopPropagation(ev);  
  3.      this.preventDefault(ev);  
  4.  },  
  5.   
  6.  stopPropagation : function(ev) {  
  7.      ev = ev.browserEvent || ev;  
  8.      if (ev.stopPropagation) {  
  9.          ev.stopPropagation();  
  10.      } else {  
  11.          ev.cancelBubble = true;  
  12.      }  
  13.  },  
  14.   
  15.  preventDefault : function(ev) {  
  16.      ev = ev.browserEvent || ev;  
  17.      if (ev.preventDefault) {  
  18.          ev.preventDefault();  
  19.      } else {  
  20.          ev.returnValue = false;  
  21.      }  
  22.  },  

 preventDefault 为阻止事件的默认行为。W3C标准使用 preventDefault 方法,IE6/7/8则是设置 returnValue 为false。Safari/Chrome/Opera同时支持IE6/7/8方式。Firefox仅支持标准的preventDefault。IE9两者都支持。


stopPropagation 用来停止事件冒泡,阻止事件进一步向下传播。关于事件传播的三个阶段:捕捉阶段、到达目标对象阶段、起泡阶段的详细描述可参阅《 JavaScript 权威指南》

stopEvent 则同时阻止默认行为和事件冒泡

 

下面几个方法只把源码贴出来,功能可以看注释。注意的是getListeners和purgeElement在Ext.EventManager中会实现,等讲到哪里再详细的分析

 

Js代码    收藏代码
  1.  /** 
  2.  * 获取事件对象 
  3.  */  
  4. getEvent : function(e) {  
  5.     e = e || win.event;  
  6.     if (!e) {  
  7.         var c = this.getEvent.caller;  
  8.         while (c) {  
  9.             e = c.arguments[0];  
  10.             if (e && Event == e.constructor) {  
  11.                 break;  
  12.             }  
  13.             c = c.caller;  
  14.         }  
  15.     }  
  16.     return e;  
  17. },  
  18.   
  19. /** 
  20.  * 获取按键码,注意在keypress 事件中使用。 
  21.  */  
  22. getCharCode : function(ev) {  
  23.     ev = ev.browserEvent || ev;  
  24.     return ev.charCode || ev.keyCode || 0;  
  25. },  
  26.   
  27. //clearCache: function() {},  
  28. // deprecated, call from EventManager  
  29. /** 
  30.  * 获取注册在某个事件类型上的所有监听器 
  31.  */  
  32. getListeners : function(el, eventName) {  
  33.     Ext.EventManager.getListeners(el, eventName);  
  34. },  
  35.   
  36. // deprecated, call from EventManager  
  37. /** 
  38.  * 清除注册在某个事件类型上的所有监听器 
  39.  */  
  40. purgeElement : function(el, recurse, eventName) {  
  41.     Ext.EventManager.purgeElement(el, recurse, eventName);  
  42. },  
  43.   
  44. /** 
  45.  * 如果文档已经加载完成,如果是IE,则将注册在 window 的 onload 事件上的监听器全部清除掉 
  46.  */  
  47. _load : function(e) {  
  48.     loadComplete = true;  
  49.       
  50.     if (Ext.isIE && e !== true) {  
  51.         // IE8 complains that _load is null or not an object  
  52.         // so lets remove self via arguments.callee  
  53.         doRemove(win, "load", arguments.callee);  
  54.     }  
  55. },  
  56.   
  57. /** 
  58.  * 在文档卸载之前,把注册在 window 的 unload 事件上的所有监听函数执行一遍,然后从缓存数组中清除掉。 
  59.  */  
  60. _unload : function(e) {  
  61.      var EU = Ext.lib.Event,  
  62.         i, v, ul, id, len, scope;  
  63.   
  64.     for (id in unloadListeners) {  
  65.         ul = unloadListeners[id];  
  66.         for (i = 0, len = ul.length; i < len; i++) {  
  67.             v = ul[i];  
  68.             if (v) {  
  69.                 try{  
  70.                     scope = v[ADJ_SCOPE] ? (v[ADJ_SCOPE] === true ? v[OBJ] : v[ADJ_SCOPE]) :  win;  
  71.                     v[FN].call(scope, EU.getEvent(e), v[OBJ]);  
  72.                 }catch(ex){}  
  73.             }  
  74.         }  
  75.     };  
  76.   
  77.     Ext.EventManager._unload();  
  78.   
  79.     doRemove(win, UNLOAD, EU._unload);  
  80. }  
 

另外在Ext.lib.Event的开头定义了一些变量

 

Js代码    收藏代码
  1. var loadComplete = false,  
  2.         unloadListeners = {},//用来存放el的unload事件  
  3.         retryCount = 0,  
  4.         onAvailStack = [],  
  5.         _interval,  
  6.         locked = false,  
  7.         win = window,  
  8.         doc = document,  
  9.   
  10.         // constants  
  11.         POLL_RETRYS = 200,  
  12.         POLL_INTERVAL = 20,  
  13.         TYPE = 0,  
  14.         FN = 1,  
  15.         OBJ = 2,  
  16.         ADJ_SCOPE = 3,  
  17.         SCROLLLEFT = 'scrollLeft',  
  18.         SCROLLTOP = 'scrollTop',  
  19.         UNLOAD = 'unload',  
  20.         MOUSEOVER = 'mouseover',  
  21.         MOUSEOUT = 'mouseout',  

 将window、document等外部(非函数作用域内)变量存于本地变量中,这样JS压缩程序就能对它们进行名称替换,获得更高的压缩率。

 

以上代码为ExtJs对浏览器原生事件的简单封装,而对浏览器本身的事件进行转换和调用,是通过类Ext.EventObject和Ext.EventManager实现的,请看后续讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值