前面讲了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实现的,请看后续讲解。