读Ext之四(事件的低级封装)
这篇开始读ext-base-event.js。该文件定义了Ext.lib.Event对象,Ext.lib这个命名空间在Ext core的Ext.js中命名的。
1 | Ext.ns( "Ext.util" , "Ext.lib" , "Ext.data" ); |
Ext.lib上的属性如下:
1 2 3 4 5 6 7 8 9 10 11 | Ext.lib.Ajax Ext.lib.Anim Ext.lib.AnimMgr Ext.lib.Bezier Ext.lib.Dom Ext.lib.Easing Ext.lib.Event Ext.lib.AnimBase Ext.lib.ColorAnim Ext.lib.Motion Ext.lib.Scroll |
Ext.lib.Event 是Ext中事件处理的轻度封装,概览下
1 2 3 4 5 6 | Ext.lib.Event = function () { var loadComplete = false , ... ... return pub; }(); |
可以发现仍然是一个匿名函数执行,执行后返回对象pub,pub赋值给Ext.lib.Event。再看内部细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var loadComplete = false , unloadListeners = {}, retryCount = 0, onAvailStack = [], _interval, locked = false , win = window, doc = document, // constants POLL_RETRYS = 200, POLL_INTERVAL = 20, EL = 0, TYPE = 0, FN = 1, WFN = 2, OBJ = 2, ADJ_SCOPE = 3, SCROLLLEFT = 'scrollLeft' , SCROLLTOP = 'scrollTop' , UNLOAD = 'unload' , MOUSEOVER = 'mouseover' , MOUSEOUT = 'mouseout' , |
以上定义了一堆变量。window,document对象分别赋值给了win,doc。这样做的好处是减少了一层闭包。使用局部变量win,doc比直接使用window,document要快。因为它们存在于执行函数的活动对象中,解析标识符只需要查找作用域链中的单个对象。
而读取变量值的耗时是随着查找作用域链的逐层深入而不断增加。这点可参考:《JS权威指南》第五版4.7节:深入理解变量作用域。
doc后是一堆常量定义,Ext的编码习惯亦是常量全部使用大写,有多个单词时用下划线连接。接下来是一堆私有方法/函数定义,即这些函数只能在上面提到的最外层的匿名函数内使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // private 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) { ret = function (el, eventName, fn, capture) { el.attachEvent( "on" + eventName, fn); return fn; }; } else { ret = function (){}; } return ret; }(), |
doAdd,亦是一个匿名函数执行后返回新函数,用来给html元素添加事件及事件响应函数(handler)。这个函数和多数的事件添加函数差不多,用特性判断 。标准浏览器使用addEventListener添加,IE系列使用attachEvent,都不支持则返回一个空函数。这里有几点,
1,有的代码中使用特性判断时,先写win.attachEvent,后是win.addEventListener。这是不对的,应该优先使用标准的addEventListener,而IE9同时支持这两种方式。
2,这里新增了mouseenter /mouseleave 事件,它们仅IE支持。mouseenter不同于mouseover,它是在第一次鼠标进入节点区域时触发,以后在节点区域内(子节点间)移动时不触发。Goodbye mouseover, hello mouseenter 详细讲述了使用mouseenter的好处。此处有简单的实现。
这里为非IE浏览器间接实现了这两个事件,需要另两个函数的辅助
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function checkRelatedTarget(e) { return !elContains(e.currentTarget, pub.getRelatedTarget(e)); } function elContains(parent, child) { if (parent && parent.firstChild){ while (child) { if (child === parent) { return true ; } child = child.parentNode; if (child && (child.nodeType != 1)) { child = null ; } } } return false ; } |
elContains 两个参数parent,child判断某个元素child是否是parent的子元素,是则返回true,否则false。
checkRelatedTarget 会作为一个拦截器,这里e.currentTarget IE6/7/8不支持。pub.getRelatedTarget(e)是下面封装好的方法,IE中使用fromElement,toElement。
1 | fn = fn.createInterceptor(checkRelatedTarget); |
实现的基本思路:使用mouseover事件,即当给某元素(parent)添加mouseenter事件时,鼠标移至parent时触发事件handler,但从其子元素上移动时并不触发。
顺便提下,Ext这里的elContains方法的实现明显欠妥,实际上IE中可以使用contains ,现代浏览器则可使用compareDocumentPosition ,谢谢天堂 提醒。John 写了个
1 2 3 4 5 | function contains(a, b){ return a.contains ? a != b && a.contains(b) : !!(a.compareDocumentPosition(b) & 16); } |
jQuery的选择器Sizzle.contains也是这么实现。
1 2 3 4 5 6 7 8 9 10 11 | 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]。
1 2 3 4 5 6 7 8 9 10 11 | 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计算得到。
IE9实际上也可通过clientX/Y获取,这里判断浏览器Ext.isIE在IE9正式版即将发布后明显欠妥。
再往下就是一个对象pub,匿名函数执行后会返回该对象。猜测pub是public的简写,即匿名函数执行后对外公开的接口对象(pub)。pub有以下方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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[id].splice(i, 1); } } } return ; } doRemove(el, eventName, fn, false ); } }, |
删除元素已注册的事件响应函数,参数同addListener。
这两个函数都有个注释:This function should ALWAYS be called from Ext.EventManager
可以发现,真正客户端程序员在使用Ext库时并不直接使用Ext.lib.Event.addListener / Ext.lib.Event.removeListener添加或删除事件。
而是使用Ext.EventManager.addListener / Ext.EventManager.removeListener或者它们的缩写Ext.EventManager.on / Ext.EventManager.un。
Ext.EventManager对事件管理提供了更高层次的封装。后续会介绍。
1 2 3 4 | getTarget : function (ev) { ev = ev.browserEvent || ev; return this .resolveTextNode(ev.target || ev.srcElement); }, |
获取事件源对象。W3C标准使用 target ,IE6/7/8使用了专有的 srcElement 。令人惊奇的是Safari/Chrome/Opera也支持IE6/7/8方式,即同时支持标准和IE专有方式。Firefox仅支持标准的target,IE9beta现已支持target。
1 2 3 4 5 6 | getRelatedTarget : function (ev) { ev = ev.browserEvent || ev; return this .resolveTextNode(ev.relatedTarget || (ev.type == MOUSEOUT ? ev.toElement : ev.type == MOUSEOVER ? ev.fromElement : null )); }, |
获取事件相关的元素。W3C标准使用 relatedTarget ,IE6/7/8使用了专有的 fromElement / toElement 。同样Safari/Chrome/Opera也支持IE6/7/8方式,即同时支持标准和IE专有方式。Firefox仅支持标准的relatedTarget,IE9也已支持relatedTarget。
1 2 3 4 5 6 7 8 9 | getPageX : function (ev) { return getPageCoord(ev, "X" ); }, getPageY : function (ev) { return getPageCoord(ev, "Y" ); }, getXY : function (ev) { return [ this .getPageX(ev), this .getPageY(ev)]; }, |
getPageX,getPageY调用私有的getPageCoord,getPageCoord介绍如上。getXY调用getPageX,getPageY。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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,阻止元素的默认行为。如链接A点击,默认会跳转;input[type=submit]点击,默认会提交表单。
W3C标准使用 preventDefault 方法,IE6/7/8则是设置 returnValue 为false。Safari/Chrome/Opera同时支持IE6/7/8方式。Firefox仅支持标准的preventDefault。IE9现已支持preventDefault。
stopPropagation 用来停止事件冒泡。W3C标准使用stopPropagation,IE6/7/8则是设置 cancelBubble 为true。
Safari/Chrome/Opera/Firefox也支持IE方式取消冒泡。目前为止这是Firefox唯一的一个支持IE方式的属性。IE9beta现已支持stopPropagation。
stopEvent则同时阻止默认行为和事件冒泡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; }, |
getEvent顾名思义获取事件对象。W3C标准使用响应函数的第一个参数获取,IE6/7/8则使用window.event获取。
Safari/Chrome/Opera也支持IE6/7/8方式获取,IE9beta已支持W3C标准方式获取。
关于各种情形下事件对象的获取见:获取事件对象的全家。
1 2 3 4 | getCharCode : function (ev) { ev = ev.browserEvent || ev; return ev.charCode || ev.keyCode || 0; }, |
获取按键码,注意在keypress 事件中使用。键盘事件DOM2中压根没有标准化,见:Key events
因此各浏览器自行实现,Firefox/Safari/Chrome/IE9beta支持charCode,IE6/7/8/Opera不支持但使用keyCode替代。
1 2 3 4 5 6 7 8 | getListeners : function (el, eventName) { Ext.EventManager.getListeners(el, eventName); }, // deprecated, call from EventManager purgeElement : function (el, recurse, eventName) { Ext.EventManager.purgeElement(el, recurse, eventName); }, |
这两个方法在后续讲述。
再下对load, unload做了单独处理。
Ext.lib.Event完毕。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
2011-04-26 仅IE9/10/(Opera)同时支持script元素的onload和onreadystatechange事件
2011-04-26 各浏览器对link标签onload/onreadystatechange事件支持的差异
2011-04-26 处理超时-Ajax之五
2011-04-26 检查超时-Ajax之四