前面讲了ExtJs核心代码以及扩展后,今天来说说ExtJs的事件机制,要想弄明白ExtJs的事件机制,就必须先知道浏览器的事件机制,这里给出了浏览器事件机制与自定义事件的实现 。
首先看源码 ext-base-event.js 关于浏览器本身事件的封装。代码中实现了各主要浏览器的兼容,以及对一些事件进行了扩展。该代码中首先定义了类Ext.lib.Event,该类(函数)是一个匿名函数自执行,执行后返回对象pub,pub赋值给Ext.lib.Event。
- Ext.lib.Event = function() {
- var loadComplete = false,
- unloadListeners = {},//用来存放el的unload事件
- …
- var pub = {
- …
- };
- return pub;
- }();
既然该类是围绕pub来实现的,我们首先来看pub的定义,pub中定义了许多关于事件处理的方法
- onAvailable : function(p_id, p_fn, p_obj, p_override) {
- onAvailStack.push({
- id: p_id,
- fn: p_fn,
- obj: p_obj,
- override: p_override,
- checkReady: false });
- retryCount = POLL_RETRYS;
- startInterval();
- },
该方法中调用了startInterval()和_tryPreloadAttach()。_tryPreloadAttach() 、 onAvailable() 、startInterval() 这三个函数的执行机制大致是这样的:在文档还没有加载完成之前,可以通过 onAvailable() 方法给某个对象注册某类事件的监听器, onAvailable() 方法会调用 startInterval() 方法来启动一个轮询来执行 _tryPreloadAttach() ,轮询的周期是 POLL_INTERVAL (默认是 20ms ),轮询次数是 POLL_RETRYS ( 200 次)(这么来看的话,这种依靠不断轮询来尝试文档是否已经加载完成的方法最长是 20ms*200=4 秒钟)。 _tryPreloadAttach() 方法里面会判断文档是不是已经加载完成,如果加载完成,执行注册的监听器,并把定时器清除掉。_tryPreloadAttach() 方法还有一个 tryAgain 标志用来说明是不是要进行再次尝试。
- addListener: function(el, eventName, fn) {
- el = Ext.getDom(el);
- if (el && fn) {
- if (eventName == UNLOAD) {
- if (unloadListeners[el.id] === undefined) {
- unloadListeners[el.id] = [];
- }
- unloadListeners[el.id].push([eventName, fn]);
- return fn;
- }
- return doAdd(el, eventName, fn, false);
- }
- return false;
- },
该方法为元素添加注册事件,el为添加事件的元素,eventName为事件名称(如click),fn为响应函数(hanlder)。对“unload”事件做了单独处理,内部调用了私有的doAdd函数。
- doAdd = function() {
- var ret;
- if (win.addEventListener) {//标准浏览器
- ret = function(el, eventName, fn, capture) {
- if (eventName == 'mouseenter') {
- fn = fn.createInterceptor(checkRelatedTarget);
- el.addEventListener(MOUSEOVER, fn, (capture));
- } else if (eventName == 'mouseleave') {
- fn = fn.createInterceptor(checkRelatedTarget);
- el.addEventListener(MOUSEOUT, fn, (capture));
- } else {
- el.addEventListener(eventName, fn, (capture));
- }
- return fn;
- };
- } else if (win.attachEvent) {//ie浏览器,ie9中会同时支持这两种方式win.attachEvent和win.addEventListener,
- ret = function(el, eventName, fn, capture) {
- el.attachEvent("on" + eventName, fn);
- return fn;
- };
- } else {
- ret = function(){};
- }
- return ret;
- }(),
该函数为闭包函数,即自执行函数,会返回ret对应的函数。并且添加了其他浏览器(IE除外)不支持的事件mouseenter和mouseleave ,为非IE浏览器间接实现了这两个事件,需要另两个函数的辅助。这两个辅助函数的实现也可以用到其他没有引入ExtJs的项目中。
- function checkRelatedTarget(e) {
- return !elContains(e.currentTarget, pub.getRelatedTarget(e));
- }
- //判断某个元素child是否是parent的子元素,是则返回true,否则false。
- function elContains(parent, child) {
- if(parent && parent.firstChild){
- while(child) {
- if(child === parent) {
- return true;
- }
- child = child.parentNode;
- //nodeType 属性返回被选节点的节点类型。等于1为节点Element,当child不是parent的孩子节点时,会一直执行,直到child.nodeType 为 9 document时
- if(child && (child.nodeType != 1)) {
- child = null;
- }
- }
- }
- return false;
- }
elContains 两个参数parent,child判断某个元素child是否是parent的子元素,是则返回true,否则false。
checkRelatedTarget 会作为一个拦截器,这里e.currentTarget是指添加事件的元素本身。pub.getRelatedTarget(e)返回的是跟该事件相关的元素标准浏览器用relatedTarget IE中用fromElement,toElement。在Ext.lib.Dom中也有个实现判断父子元素的方法isAncestor,后续会讲到。不知道ExtJs为什么要写两个实现,个人推测这两段代码可能是两个人实现的,并且实现的原理有些不同,故保留了两个。
下面看getRelatedTarget的实现
- getRelatedTarget : function(ev) {
- ev = ev.browserEvent || ev;
- return this.resolveTextNode(ev.relatedTarget ||
- (/(mouseout|mouseleave)/.test(ev.type) ? ev.toElement :
- /(mouseover|mouseenter)/.test(ev.type) ? ev.fromElement : null));
- },
这里有几个浏览器事件对象属性需说明一下
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辅助函数又判断了火狐和其他浏览器的不同。
- resolveTextNode : Ext.isGecko ? function(node){
- if(!node){
- return;
- }
- // work around firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=101197
- var s = HTMLElement.prototype.toString.call(node);
- if(s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]'){
- return;
- }
- return node.nodeType == 3 ? node.parentNode : node;
- } : function(node){
- return node && node.nodeType == 3 ? node.parentNode : node;
- },
下面看与addListener对应的方法removeListener
- /**
- * 删除 el 事件
- */
- removeListener: function(el, eventName, fn) {
- el = Ext.getDom(el);
- var i, len, li, lis;
- if (el && fn) {
- if(eventName == UNLOAD){
- if((lis = unloadListeners[el.id]) !== undefined){
- for(i = 0, len = lis.length; i < len; i++){
- if((li = lis[i]) && li[TYPE] == eventName && li[FN] == fn){
- unloadListeners[el.id].splice(i, 1);
- }
- }
- }
- return;
- }
- doRemove(el, eventName, fn, false);
- }
- },
该方法与addListener对应,用来删除由addListener注册的事件,实际调用是利用Ext.EventManager中的方法来调用,该类对这两个方法又进行了更进一步的封装,后续待分析。该方法中用到了辅助函数doRemove
- /**
- * 返回一个符合当前浏览器的函数用来注销事件,
- 对于非ie下的浏览器也让其可以支持mouseleave/mouseenter
- */
- doRemove = function(){
- var ret;
- if (win.removeEventListener) {
- ret = function (el, eventName, fn, capture) {
- if (eventName == 'mouseenter') {
- eventName = MOUSEOVER;
- } else if (eventName == 'mouseleave') {
- eventName = MOUSEOUT;
- }
- el.removeEventListener(eventName, fn, (capture));
- };
- } else if (win.detachEvent) {
- ret = function (el, eventName, fn) {
- el.detachEvent("on" + eventName, fn);
- };
- } else {
- ret = function(){};
- }
- return ret;
- }();
除了注册和删除事件,Ext还对其他原生的事件方法(属性)进行了封装,看下面
- getTarget : function(ev) {
- ev = ev.browserEvent || ev;
- return this.resolveTextNode(ev.target || ev.srcElement);
- },
获取当前事件源对象
- getPageX : function(ev) {
- return getPageCoord(ev, "X");
- },
- getPageY : function(ev) {
- return getPageCoord(ev, "Y");
- },
- getXY : function(ev) {
- return [this.getPageX(ev), this.getPageY(ev)];
- },
这三个函数用来获取鼠标事件源的坐标(水平,垂直)坐标,其中调用了辅助函数getPageCoord和getScroll,该方法中解决了不同浏览器之间的不同,实现了兼容。
- function getPageCoord (ev, xy) {
- ev = ev.browserEvent || ev;
- var coord = ev['page' + xy];
- if (!coord && coord !== 0) {
- coord = ev['client' + xy] || 0;
- if (Ext.isIE) {
- coord += getScroll()[xy == "X" ? 0 : 1];
- }
- }
- return coord;
- }
私有的getPageCoord方法用来获取鼠标事件时相对于文档的坐标(水平,垂直)。
Firefox引入了pageX / Y ,IE9/Safari/Chrome/Opera虽然支持但仅在文档(document)内而非页面(page)。
Safari/Chrome/Opera可以使用标准的clientX/Y获取,IE下可通过clientX/Y与scrollLeft/scrollTop计算得到。
- function getScroll() {
- var dd = doc.documentElement,
- db = doc.body;
- if(dd && (dd[SCROLLTOP] || dd[SCROLLLEFT])){
- return [dd[SCROLLLEFT], dd[SCROLLTOP]];
- }else if(db){
- return [db[SCROLLLEFT], db[SCROLLTOP]];
- }else{
- return [0, 0];
- }
- }
私有的getScroll方法返回文档的scrollTop和scrollLeft值,由于浏览器差异,该实现上先从document.documentElement取,为0后再从document.body上取。都没有的话返回[0,0]。
下面看另外三个方法
- stopEvent : function(ev) {
- this.stopPropagation(ev);
- this.preventDefault(ev);
- },
- stopPropagation : function(ev) {
- ev = ev.browserEvent || ev;
- if (ev.stopPropagation) {
- ev.stopPropagation();
- } else {
- ev.cancelBubble = true;
- }
- },
- preventDefault : function(ev) {
- ev = ev.browserEvent || ev;
- if (ev.preventDefault) {
- ev.preventDefault();
- } else {
- ev.returnValue = false;
- }
- },
preventDefault 为阻止事件的默认行为。W3C标准使用 preventDefault 方法,IE6/7/8则是设置 returnValue 为false。Safari/Chrome/Opera同时支持IE6/7/8方式。Firefox仅支持标准的preventDefault。IE9两者都支持。
stopPropagation 用来停止事件冒泡,阻止事件进一步向下传播。关于事件传播的三个阶段:捕捉阶段、到达目标对象阶段、起泡阶段的详细描述可参阅《 JavaScript 权威指南》
stopEvent 则同时阻止默认行为和事件冒泡
下面几个方法只把源码贴出来,功能可以看注释。注意的是getListeners和purgeElement在Ext.EventManager中会实现,等讲到哪里再详细的分析
- /**
- * 获取事件对象
- */
- getEvent : function(e) {
- e = e || win.event;
- if (!e) {
- var c = this.getEvent.caller;
- while (c) {
- e = c.arguments[0];
- if (e && Event == e.constructor) {
- break;
- }
- c = c.caller;
- }
- }
- return e;
- },
- /**
- * 获取按键码,注意在keypress 事件中使用。
- */
- getCharCode : function(ev) {
- ev = ev.browserEvent || ev;
- return ev.charCode || ev.keyCode || 0;
- },
- //clearCache: function() {},
- // deprecated, call from EventManager
- /**
- * 获取注册在某个事件类型上的所有监听器
- */
- getListeners : function(el, eventName) {
- Ext.EventManager.getListeners(el, eventName);
- },
- // deprecated, call from EventManager
- /**
- * 清除注册在某个事件类型上的所有监听器
- */
- purgeElement : function(el, recurse, eventName) {
- Ext.EventManager.purgeElement(el, recurse, eventName);
- },
- /**
- * 如果文档已经加载完成,如果是IE,则将注册在 window 的 onload 事件上的监听器全部清除掉
- */
- _load : function(e) {
- loadComplete = true;
- if (Ext.isIE && e !== true) {
- // IE8 complains that _load is null or not an object
- // so lets remove self via arguments.callee
- doRemove(win, "load", arguments.callee);
- }
- },
- /**
- * 在文档卸载之前,把注册在 window 的 unload 事件上的所有监听函数执行一遍,然后从缓存数组中清除掉。
- */
- _unload : function(e) {
- var EU = Ext.lib.Event,
- i, v, ul, id, len, scope;
- for (id in unloadListeners) {
- ul = unloadListeners[id];
- for (i = 0, len = ul.length; i < len; i++) {
- v = ul[i];
- if (v) {
- try{
- scope = v[ADJ_SCOPE] ? (v[ADJ_SCOPE] === true ? v[OBJ] : v[ADJ_SCOPE]) : win;
- v[FN].call(scope, EU.getEvent(e), v[OBJ]);
- }catch(ex){}
- }
- }
- };
- Ext.EventManager._unload();
- doRemove(win, UNLOAD, EU._unload);
- }
另外在Ext.lib.Event的开头定义了一些变量
- var loadComplete = false,
- unloadListeners = {},//用来存放el的unload事件
- retryCount = 0,
- onAvailStack = [],
- _interval,
- locked = false,
- win = window,
- doc = document,
- // constants
- POLL_RETRYS = 200,
- POLL_INTERVAL = 20,
- TYPE = 0,
- FN = 1,
- OBJ = 2,
- ADJ_SCOPE = 3,
- SCROLLLEFT = 'scrollLeft',
- SCROLLTOP = 'scrollTop',
- UNLOAD = 'unload',
- MOUSEOVER = 'mouseover',
- MOUSEOUT = 'mouseout',
将window、document等外部(非函数作用域内)变量存于本地变量中,这样JS压缩程序就能对它们进行名称替换,获得更高的压缩率。
以上代码为ExtJs对浏览器原生事件的简单封装,而对浏览器本身的事件进行转换和调用,是通过类Ext.EventObject和Ext.EventManager实现的,请看后续讲解。