本篇开始将回顾下Javascript的事件机制。同时会从一个最小的函数开始写到最后一个具有完整功能的,强大的事件模块。为叙述方便将响应函数/回调函数/事件Listener/事件handler都称为事件handler。
<div onclick="alert('欢迎访问Nowamagic.net');">Nowamagic</div>
<script type="text/javascript"> function clk(){} </script> <div onclick="clk()">Div2 Element</div>
<div id="d3">Div3 Element</div> <script type="text/javascript"> var d3 = document.getElementById('d3'); d3.onclick = function(){ } </script>
<div id="d4">Div4 Element</div> <script type="text/javascript"> var d4 = document.getElementById('d4'); function clk(){alert(4)} if(d4.addEventListener){ d4.addEventListener('click',clk,false); } if(d4.attachEvent){ d4.attachEvent('onclick',clk); } </script>
好,把方式4简单的封装下, 兼容标准浏览器及IE浏览器。注意attachEvent的第一个参数需要加上个"on",addEventListener第三个参数为false表示事件冒泡,attachEvent没有第三个参数,默认就是冒泡,没有捕获。
/** * * @param {Object} el HTML Element * @param {Object} type 事件类型 * @param {Object} fn 事件handler */ function addEvent(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el.attachEvent('on' + type, fn); } }
好,用这个工具函数添加一个给document添加一个点击事件:
function handler(){ alert(this); alert(arguments[0]); } addEvent(document, 'click', handler);
在Firefox等标准浏览器中,点击页面后将弹出 "[object HTMLDocument]",及handler中的this就是document自身。但在IE6/7/8中this却是window对象。这让人不爽,修改下与标准浏览器统一。
function addEvent(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el['e' + fn] = function(){ fn.call(el, window.event); } el.attachEvent('on'+type, el['e'+fn]); } }
上面我们封装了一个addEvent,解决了IE6/7/8下事件handler中this为window的错误,并且统一了事件对象作为事件handler的第一个参数传入。
这篇把对应的删除事件的函数补上。上一篇中fn在IE6/7/8中实际上被包装了,IE6/7/8中真正的handler是el["e"+fn]。因此删除时要用到它。同时将两个方法挂在一个对象E上,add,remove分别添加和删除事件。
E = { //添加事件 add : function(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el['e'+fn] = function(){ fn.call(el,evt); }; el.attachEvent('on' + type, el['e'+fn]); } }, //删除事件 remove : function(el, type, fn){ if(el.removeEventListener){ el.removeEventListener(type, fn, false); }else if(el.detachEvent){ el.detachEvent('on' + type, el['e'+fn]); } } };
可以看到,标准浏览器如IE9/Firefox/Safari/Chrome/Opera会使用addEventListener/removeEventListener添加/删除事件,IE6/7/8则使用attachEvent/detachEvent。标准浏览器中事件handler是传入的第三个参数fn,IE6/7/8中则是包装后的el["e"+fn]。
好了,已经拥有了添加,删除事件两个方法,并且解决了各浏览器下中的部分差异,现再添加一个主动触发事件的方法dispatch。该方法能模拟用户行为,如点击(click)操作等。 标准使用dispatchEvent方法,IE6/7/8则使用fireEvent方法。因为可能会出现异常,使用了try catch。
E = { //添加事件 add : function(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el['e'+fn] = function(){ fn.call(el,window.event); }; el.attachEvent('on' + type, el['e'+fn]); } }, //删除事件 remove : function(el, type, fn){ if(el.removeEventListener){ el.removeEventListener(type, fn, false); }else if(el.detachEvent){ el.detachEvent('on' + type, el['e'+fn]); } }, //主动触发事件 dispatch : function(el ,type){ try{ if(el.dispatchEvent){ var evt = document.createEvent('Event'); evt.initEvent(type,true,true); el.dispatchEvent(evt); }else if(el.fireEvent){ el.fireEvent('on'+type); } }catch(e){}; } };
这就是整个事件模块的雏形,往后还有很多需要补充完善的地方。但对于普通的应用,这几个函数足以胜任。
上面的add有个问题,对同一类型事件添加多个hanlder时,IE6/7/8下会无序,如
<div id="d1" style="width:200px;height:200px;background:gold;"></div> <script type="text/javascript"> var el = document.getElementById('d1'); function handler1(){alert('1');} function handler2(){alert('2');} function handler3(){alert('3');} function handler4(){alert('4');} function handler5(){alert('5');} E.add(el, 'click', handler1); E.add(el, 'click', handler2); E.add(el, 'click', handler3); E.add(el, 'click', handler4); E.add(el, 'click', handler5); </script>
IE9/Firefox/Safari/Chomre/Opera会依次输出1,2,3,4,5。但IE6/7/8中则不一定。为解决所有浏览器中多个事件handler有序执行,我们需要一个队列来管理所有的handler。
这次,把所有的内部细节封装在一个匿名函数中,该函数执行完毕后返回如上一篇接口相同的方法。另外
E = function(){ function _isEmptyObj(obj){ for(var a in obj){ return false; } return true; } function _each(ary, callback){ for(var i=0,len=ary.length; i<len;){ callback(i, ary[i]) ? i=0 : i++; } } function _remove(el, type){ var handler = el.listeners[type]['_handler_']; el.removeEventListener ? el.removeEventListener(type, handler, false) : el.detachEvent('on'+type, handler); delete el.listeners[type]; if(_isEmptyObj(el.listeners)){ delete el.listeners; } } // 添加事件 function add(el, type, fn){ el.listeners = el.listeners || {}; var listeners = el.listeners[type] = el.listeners[type] || []; listeners.push(fn); if(!listeners['_handler_']){ listeners['_handler_'] = function(e){ var evt = e || window.event; for(var i=0,fn; fn=listeners[i++];){ fn.call(el, evt); } } el.addEventListener ? el.addEventListener(type, listeners['_handler_'], false) : el.attachEvent('on' + type, listeners['_handler_']); } } // 删除事件 function remove(el, type, fn){ if(!el.listeners) return; var listeners = el.listeners && el.listeners[type]; if(listeners) { _each(listeners, function(i, f){ if(f==fn){ return listeners.splice(i, 1); } }); if(listeners.length == 0){ _remove(el,type); } } } //主动触发事件 function dispatch(el ,type){ try{ if(el.dispatchEvent){ var evt = document.createEvent('Event'); evt.initEvent(type,true,true); el.dispatchEvent(evt); }else if(el.fireEvent){ el.fireEvent('on'+type); } }catch(e){}; } return { add: add, remove: remove, dispatch: dispatch }; }();
上面解决了IE6/7/8中同一个类型事件的多个handler执行无序的情况,为此改动也是较大的。实现几乎与前一个版本完全不同。但好处也是明显的。
有时需要添加只执行一次的事件handler,为此给add方法添加第四个参数one,one为true则该事件handler只执行一次。
<div id="d1" style="width:200px;height:200px;background:gold;"></div> <script> var el = document.getElementById('d1'); function handler(){alert(5)} E.add(el, 'click', handler, true); </script>
再扩展下remove函数。
比如当给一个el添加了3个click事件的handler,1个mouseover事件的handler
function handler1(){alert('1');} function handler2(){alert('2');} function handler3(){alert('3');} function handler4(){alert('4');} E.add(el, 'click', f1); E.add(el, 'click', f2); E.add(el, 'click', f3); E.add(el, 'mouseover', f4);
使用以下语句将删除元素click的所有handler:E.remove(el, 'click');
以下将删除元素身上所有的事件handler,包括click和mouseover:E.remove(el);
上面正式推出了我的事件模块event_v1,已经搭起了它的初始框架。或许有人要说,与众多JS库或框架相比,它还没有解决事件对象的兼容性问题。是的,我故意将此放到后续补充。因为事件对象的兼容性问题太多了,太繁琐了。
下面我将引入一个私有的_fixEvent函数,add中将调用该函数。_fixEvent将修复(或称包装)原生事件对象,返回一个标准的统一接口的事件对象。如下
function _fixEvent( evt, el ) { var props = "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), len = props.length; function now() {return (new Date).getTime();} function returnFalse() {return false;} function returnTrue() {return true;} function Event( src ) { this.originalEvent = src; this.type = src.type; this.timeStamp = now(); } Event.prototype = { preventDefault: function() { this.isDefaultPrevented = returnTrue; var e = this.originalEvent; if( e.preventDefault ) { e.preventDefault(); } e.returnValue = false; }, stopPropagation: function() { this.isPropagationStopped = returnTrue; var e = this.originalEvent; if( e.stopPropagation ) { e.stopPropagation(); } e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); }, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse }; var originalEvent = evt; evt = new Event( originalEvent ); for(var i = len, prop; i;) { prop = props[ --i ]; evt[ prop ] = originalEvent[ prop ]; } if(!evt.target) { evt.target = evt.srcElement || document; } if( evt.target.nodeType === 3 ) { evt.target = evt.target.parentNode; } if( !evt.relatedTarget && evt.fromElement ) { evt.relatedTarget = evt.fromElement === evt.target ? evt.toElement : evt.fromElement; } if( evt.pageX == null && evt.clientX != null ) { var doc = document.documentElement, body = document.body; evt.pageX = evt.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); evt.pageY = evt.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); } if( !evt.which && ((evt.charCode || evt.charCode === 0) ? evt.charCode : evt.keyCode) ) { evt.which = evt.charCode || evt.keyCode; } if( !evt.metaKey && evt.ctrlKey ) { evt.metaKey = evt.ctrlKey; } if( !evt.which && evt.button !== undefined ) { evt.which = (evt.button & 1 ? 1 : ( evt.button & 2 ? 3 : ( evt.button & 4 ? 2 : 0 ) )); } if(!evt.currentTarget) evt.currentTarget = el; return evt; }
好了,现在你要
更多的差异性,不在这一一列举了。