深入研究JavaScript的事件机制
2014-10-23 09:01
369 查看
本篇开始将回顾下Javascript的事件机制。同时会从一个最小的函数开始写到最后一个具有完整功能的,强大的事件模块。为叙述方便将响应函数/回调函数/事件Listener/事件handler都称为事件handler。
先看看页面中添加事件的几种方式:
直接将JS代码写在HTML上
测试:Nowamagic
viewsource
print?
HTMLElement元素自身就拥有了很多onXXX属性,只需将JS代码赋值给其就可以了。赋值给onXXX的字符串将作为响应函数的函数体(FunctionBody)。大概这是上世纪90年代的写法,那时候直接把JS代码写在网页中很普遍,也许那时候的JS并不太重要,只是用来做做验证或一些花哨的效果而已。
定义一个函数,赋值给html元素的onXXX属性
viewsource
print?
先定义函数clk,然后赋值给onclick属性,这种方式也应该属于上世纪90年代的流行写法。比第一种方式好的是它把业务逻辑代码都封装在一个函数里了,使HTML代码与JS代码稍微有点儿分离,不至于第一种那么紧密耦合。
使用element.onXXX方式
viewsource
print?
这种方式也是早期的写法,但好处是可以将JS与HTML完全分离,前提是需要给HTML元素提供一个额外的id属性(或其它能获取该元素对象的方式)。
使用addEventListener或attachEvent
viewsource
print?
这是目前推荐的方式,较前两种方式功能更为强大,可以为元素添加多个事件handler,支持事件冒泡或捕获,前三种方式默认都是冒泡。IE6/7/8仍然没有遵循标准而使用了自己专有的attachEvent,且不支持事件捕获。
好,把方式4简单的封装下,兼容标准浏览器及IE浏览器。注意attachEvent的第一个参数需要加上个"on",addEventListener第三个参数为false表示事件冒泡,attachEvent没有第三个参数,默认就是冒泡,没有捕获。
viewsource
print?
好,用这个工具函数添加一个给document添加一个点击事件:
viewsource
print?
在Firefox等标准浏览器中,点击页面后将弹出"[objectHTMLDocument]",及handler中的this就是document自身。但在IE6/7/8中this却是window对象。这让人不爽,修改下与标准浏览器统一。
viewsource
print?
上面我们封装了一个addEvent,解决了IE6/7/8下事件handler中this为window的错误,并且统一了事件对象作为事件handler的第一个参数传入。
这篇把对应的删除事件的函数补上。上一篇中fn在IE6/7/8中实际上被包装了,IE6/7/8中真正的handler是el["e"+fn]。因此删除时要用到它。同时将两个方法挂在一个对象E上,add,remove分别添加和删除事件。
viewsource
print?
可以看到,标准浏览器如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方法。因为可能会出现异常,使用了trycatch。
viewsource
print?
这就是整个事件模块的雏形,往后还有很多需要补充完善的地方。但对于普通的应用,这几个函数足以胜任。
上面的add有个问题,对同一类型事件添加多个hanlder时,IE6/7/8下会无序,如
viewsource
print?
IE9/Firefox/Safari/Chomre/Opera会依次输出1,2,3,4,5。但IE6/7/8中则不一定。为解决所有浏览器中多个事件handler有序执行,我们需要一个队列来管理所有的handler。
这次,把所有的内部细节封装在一个匿名函数中,该函数执行完毕后返回如上一篇接口相同的方法。另外
把真正的事件handler挂在el上,即el.listeners,其为一个对象,每一个类型的事件为一个数组,如click为el.listeners["click"]=[]。
所有的handler存在在对于的数组中
删除一个hanlder,将从数组中将其删除
viewsource
print?
上面解决了IE6/7/8中同一个类型事件的多个handler执行无序的情况,为此改动也是较大的。实现几乎与前一个版本完全不同。但好处也是明显的。
有时需要添加只执行一次的事件handler,为此给add方法添加第四个参数one,one为true则该事件handler只执行一次。
viewsource
print?
再扩展下remove函数。
删除元素type类型的所有监听器(参数传el,type)
删除元素所有的监听器(仅传el)
比如当给一个el添加了3个click事件的handler,1个mouseover事件的handler
viewsource
print?
使用以下语句将删除元素click的所有handler:E.remove(el,'click');
以下将删除元素身上所有的事件handler,包括click和mouseover:E.remove(el);
上面正式推出了我的事件模块event_v1,已经搭起了它的初始框架。或许有人要说,与众多JS库或框架相比,它还没有解决事件对象的兼容性问题。是的,我故意将此放到后续补充。因为事件对象的兼容性问题太多了,太繁琐了。
下面我将引入一个私有的_fixEvent函数,add中将调用该函数。_fixEvent将修复(或称包装)原生事件对象,返回一个标准的统一接口的事件对象。如下
viewsource
print?
好了,现在你要
阻止事件默认行为,统一使用e.preventDefault()
停止冒泡,统一使用e.stopPropagation()
获取事件源,统一使用e.target
……/li>
更多的差异性,不在这一一列举了。
先看看页面中添加事件的几种方式:
直接将JS代码写在HTML上
测试:Nowamagic
1 | <divonclick= "alert('欢迎访问Nowamagic.net');" >Nowamagic</div> |
定义一个函数,赋值给html元素的onXXX属性
1 | <scripttype= "text/javascript" > |
2 | function clk(){} |
3 | </script> |
4 | <divonclick= "clk()" >Div2Element</div> |
使用element.onXXX方式
1 | <divid= "d3" >Div3Element</div> |
2 | <scripttype= "text/javascript" > |
3 | var d3=document.getElementById( 'd3' ); |
4 | d3.onclick= function (){} |
5 | </script> |
使用addEventListener或attachEvent
01 | <divid= "d4" >Div4Element</div> |
02 | <scripttype= "text/javascript" > |
03 | var d4=document.getElementById( 'd4' ); |
04 | function clk(){alert(4)} |
05 | if (d4.addEventListener){ |
06 | d4.addEventListener( 'click' ,clk, false ); |
07 | } |
08 | if (d4.attachEvent){ |
09 | d4.attachEvent( 'onclick' ,clk); |
10 | } |
11 | </script> |
好,把方式4简单的封装下,兼容标准浏览器及IE浏览器。注意attachEvent的第一个参数需要加上个"on",addEventListener第三个参数为false表示事件冒泡,attachEvent没有第三个参数,默认就是冒泡,没有捕获。
01 | /** |
02 | * |
03 | *@param{Object}elHTMLElement |
04 | *@param{Object}type事件类型 |
05 | *@param{Object}fn事件handler |
06 | */ |
07 | function addEvent(el,type,fn){ |
08 | if (el.addEventListener){ |
09 | el.addEventListener(type,fn, false ); |
10 | } else { |
11 | el.attachEvent( 'on' +type,fn); |
12 | } |
13 | } |
1 | function handler(){ |
2 | alert( this ); |
3 | alert(arguments[0]); |
4 | } |
5 | addEvent(document, 'click' ,handler); |
01 | function addEvent(el,type,fn){ |
02 | if (el.addEventListener){ |
03 | el.addEventListener(type,fn, false ); |
04 | } else { |
05 | el[ 'e' +fn]= function (){ |
06 | fn.call(el,window.event); |
07 | } |
08 | el.attachEvent( 'on' +type,el[ 'e' +fn]); |
09 | } |
10 | } |
这篇把对应的删除事件的函数补上。上一篇中fn在IE6/7/8中实际上被包装了,IE6/7/8中真正的handler是el["e"+fn]。因此删除时要用到它。同时将两个方法挂在一个对象E上,add,remove分别添加和删除事件。
01 | E={ |
02 | //添加事件 |
03 | add: function (el,type,fn){ |
04 | if (el.addEventListener){ |
05 | el.addEventListener(type,fn, false ); |
06 | } else { |
07 | el[ 'e' +fn]= function (){ |
08 | fn.call(el,evt); |
09 | }; |
10 | el.attachEvent( 'on' +type,el[ 'e' +fn]); |
11 | } |
12 | }, |
13 | //删除事件 |
14 | remove: function (el,type,fn){ |
15 | if (el.removeEventListener){ |
16 | el.removeEventListener(type,fn, false ); |
17 | } else if (el.detachEvent){ |
18 | el.detachEvent( 'on' +type,el[ 'e' +fn]); |
19 | } |
20 | } |
21 | }; |
好了,已经拥有了添加,删除事件两个方法,并且解决了各浏览器下中的部分差异,现再添加一个主动触发事件的方法dispatch。该方法能模拟用户行为,如点击(click)操作等。标准使用dispatchEvent方法,IE6/7/8则使用fireEvent方法。因为可能会出现异常,使用了trycatch。
01 | E={ |
02 | //添加事件 |
03 | add: function (el,type,fn){ |
04 | if (el.addEventListener){ |
05 | el.addEventListener(type,fn, false ); |
06 | } else { |
07 | el[ 'e' +fn]= function (){ |
08 | fn.call(el,window.event); |
09 | }; |
10 | el.attachEvent( 'on' +type,el[ 'e' +fn]); |
11 | } |
12 | }, |
13 | //删除事件 |
14 | remove: function (el,type,fn){ |
15 | if (el.removeEventListener){ |
16 | el.removeEventListener(type,fn, false ); |
17 | } else if (el.detachEvent){ |
18 | el.detachEvent( 'on' +type,el[ 'e' +fn]); |
19 | } |
20 | }, |
21 | //主动触发事件 |
22 | dispatch: function (el,type){ |
23 | try { |
24 | if (el.dispatchEvent){ |
25 | var evt=document.createEvent( 'Event' ); |
26 | evt.initEvent(type, true , true ); |
27 | el.dispatchEvent(evt); |
28 | } else if (el.fireEvent){ |
29 | el.fireEvent( 'on' +type); |
30 | } |
31 | } catch (e){}; |
32 | } |
33 | }; |
上面的add有个问题,对同一类型事件添加多个hanlder时,IE6/7/8下会无序,如
01 | <divid= "d1" style= "width:200px;height:200px;background:gold;" ></div> |
02 | <scripttype= "text/javascript" > |
03 | var el=document.getElementById( 'd1' ); |
04 | function handler1(){alert( '1' );} |
05 | function handler2(){alert( '2' );} |
06 | function handler3(){alert( '3' );} |
07 | function handler4(){alert( '4' );} |
08 | function handler5(){alert( '5' );} |
09 | E.add(el, 'click' ,handler1); |
10 | E.add(el, 'click' ,handler2); |
11 | E.add(el, 'click' ,handler3); |
12 | E.add(el, 'click' ,handler4); |
13 | E.add(el, 'click' ,handler5); |
14 | </script> |
这次,把所有的内部细节封装在一个匿名函数中,该函数执行完毕后返回如上一篇接口相同的方法。另外
把真正的事件handler挂在el上,即el.listeners,其为一个对象,每一个类型的事件为一个数组,如click为el.listeners["click"]=[]。
所有的handler存在在对于的数组中
删除一个hanlder,将从数组中将其删除
01 | E= function (){ |
02 | function _isEmptyObj(obj){ |
03 | for ( var a in obj){ |
04 | return false ; |
05 | } |
06 | return true ; |
07 | } |
08 | function _each(ary,callback){ |
09 | for ( var i=0,len=ary.length;i<len;){ |
10 | callback(i,ary[i])?i=0:i++; |
11 | } |
12 | } |
13 | function _remove(el,type){ |
14 | var handler=el.listeners[type][ '_handler_' ]; |
15 | el.removeEventListener? |
16 | el.removeEventListener(type,handler, false ): |
17 | el.detachEvent( 'on' +type,handler); |
18 | delete el.listeners[type]; |
19 | if (_isEmptyObj(el.listeners)){ |
20 | delete el.listeners; |
21 | } |
22 | } |
23 | //添加事件 |
24 | function add(el,type,fn){ |
25 | el.listeners=el.listeners||{}; |
26 | var listeners=el.listeners[type]=el.listeners[type]||[]; |
27 | listeners.push(fn); |
28 | if (!listeners[ '_handler_' ]){ |
29 | listeners[ '_handler_' ]= function (e){ |
30 | var evt=e||window.event; |
31 | for ( var i=0,fn;fn=listeners[i++];){ |
32 | fn.call(el,evt); |
33 | } |
34 | } |
35 | el.addEventListener? |
36 | el.addEventListener(type,listeners[ '_handler_' ], false ): |
37 | el.attachEvent( 'on' +type,listeners[ '_handler_' ]); |
38 | } |
39 | } |
40 | //删除事件 |
41 | function remove(el,type,fn){ |
42 | if (!el.listeners) return ; |
43 | var listeners=el.listeners&&el.listeners[type]; |
44 | if (listeners){ |
45 | _each(listeners, function (i,f){ |
46 | if (f==fn){ |
47 | return listeners.splice(i,1); |
48 | } |
49 | }); |
50 | if (listeners.length==0){ |
51 | _remove(el,type); |
52 | } |
53 | } |
54 | } |
55 | //主动触发事件 |
56 | function dispatch(el,type){ |
57 | try { |
58 | if (el.dispatchEvent){ |
59 | var evt=document.createEvent( 'Event' ); |
60 | evt.initEvent(type, true , true ); |
61 | el.dispatchEvent(evt); |
62 | } else if (el.fireEvent){ |
63 | el.fireEvent( 'on' +type); |
64 | } |
65 | } catch (e){}; |
66 | } |
67 | return { |
68 | add:add, |
69 | remove:remove, |
70 | dispatch:dispatch |
71 | }; |
72 | }(); |
有时需要添加只执行一次的事件handler,为此给add方法添加第四个参数one,one为true则该事件handler只执行一次。
1 | <divid= "d1" style= "width:200px;height:200px;background:gold;" ></div> |
2 | <script> |
3 | var el=document.getElementById( 'd1' ); |
4 | function handler(){alert(5)} |
5 | E.add(el, 'click' ,handler, true ); |
6 | </script> |
删除元素type类型的所有监听器(参数传el,type)
删除元素所有的监听器(仅传el)
比如当给一个el添加了3个click事件的handler,1个mouseover事件的handler
1 | function handler1(){alert( '1' );} |
2 | function handler2(){alert( '2' );} |
3 | function handler3(){alert( '3' );} |
4 | function handler4(){alert( '4' );} |
5 | E.add(el, 'click' ,f1); |
6 | E.add(el, 'click' ,f2); |
7 | E.add(el, 'click' ,f3); |
8 | E.add(el, 'mouseover' ,f4); |
以下将删除元素身上所有的事件handler,包括click和mouseover:E.remove(el);
上面正式推出了我的事件模块event_v1,已经搭起了它的初始框架。或许有人要说,与众多JS库或框架相比,它还没有解决事件对象的兼容性问题。是的,我故意将此放到后续补充。因为事件对象的兼容性问题太多了,太繁琐了。
下面我将引入一个私有的_fixEvent函数,add中将调用该函数。_fixEvent将修复(或称包装)原生事件对象,返回一个标准的统一接口的事件对象。如下
01 | function _fixEvent(evt,el){ |
02 | var props= "altKeyattrChangeattrNamebubblesbuttoncancelablecharCodeclientXclientYctrlKeycurrentTargetdatadetaileventPhasefromElementhandlerkeyCodelayerXlayerYmetaKeynewValueoffsetXoffsetYoriginalTargetpageXpageYprevValuerelatedNoderelatedTargetscreenXscreenYshiftKeysrcElementtargettoElementviewwheelDeltawhich" .split( "" ), |
03 | len=props.length; |
04 | function now(){ return ( new Date).getTime();} |
05 | function returnFalse(){ return false ;} |
06 | function returnTrue(){ return true ;} |
07 | function Event(src){ |
08 | this .originalEvent=src; |
09 | this .type=src.type; |
10 | this .timeStamp=now(); |
11 | } |
12 | Event.prototype={ |
13 | preventDefault: function (){ |
14 | this .isDefaultPrevented=returnTrue; |
15 | var e= this .originalEvent; |
16 | if (e.preventDefault){ |
17 | e.preventDefault(); |
18 | } |
19 | e.returnValue= false ; |
20 | }, |
21 | stopPropagation: function (){ |
22 | this .isPropagationStopped=returnTrue; |
23 | var e= this .originalEvent; |
24 | if (e.stopPropagation){ |
25 | e.stopPropagation(); |
26 | } |
27 | e.cancelBubble= true ; |
28 | }, |
29 | stopImmediatePropagation: function (){ |
30 | this .isImmediatePropagationStopped=returnTrue; |
31 | this .stopPropagation(); |
32 | }, |
33 | isDefaultPrevented:returnFalse, |
34 | isPropagationStopped:returnFalse, |
35 | isImmediatePropagationStopped:returnFalse |
36 | }; |
37 | var originalEvent=evt; |
38 | evt= new Event(originalEvent); |
39 |
40 | for ( var i=len,prop;i;){ |
41 | prop=props[--i]; |
42 | evt[prop]=originalEvent[prop]; |
43 | } |
44 | if (!evt.target){ |
45 | evt.target=evt.srcElement||document; |
46 | } |
47 | if (evt.target.nodeType===3){ |
48 | evt.target=evt.target.parentNode; |
49 | } |
50 | if (!evt.relatedTarget&&evt.fromElement){ |
51 | evt.relatedTarget=evt.fromElement===evt.target?evt.toElement:evt.fromElement; |
52 | } |
53 | if (evt.pageX== null &&evt.clientX!= null ){ |
54 | var doc=document.documentElement,body=document.body; |
55 | evt.pageX=evt.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc&&doc.clientLeft||body&&body.clientLeft||0); |
56 | evt.pageY=evt.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc&&doc.clientTop||body&&body.clientTop||0); |
57 | } |
58 | if (!evt.which&&((evt.charCode||evt.charCode===0)?evt.charCode:evt.keyCode)){ |
59 | evt.which=evt.charCode||evt.keyCode; |
60 | } |
61 | if (!evt.metaKey&&evt.ctrlKey){ |
62 | evt.metaKey=evt.ctrlKey; |
63 | } |
64 | if (!evt.which&&evt.button!==undefined){ |
65 | evt.which=(evt.button&1?1:(evt.button&2?3:(evt.button&4?2:0))); |
66 | } |
67 | if (!evt.currentTarget)evt.currentTarget=el; |
68 | return evt; |
69 | } |
阻止事件默认行为,统一使用e.preventDefault()
停止冒泡,统一使用e.stopPropagation()
获取事件源,统一使用e.target
……/li>
更多的差异性,不在这一一列举了。
相关文章推荐
- 深入研究JavaScript的事件机制
- javascript事件机制了解与深入
- 【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)
- javascript事件机制研究
- 深入理解JavaScript事件循环机制
- javascript之-深入事件机制
- 再次深入研究Android事件分发机制,学习笔记。
- 【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)
- 【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)
- 【JavaScript 学习--12】JS深入理解调用栈,事件循环机制,回调队列
- 韩顺平 javascript教学视频_学习笔记23_js事件驱动机制深入理解_js常用事件_js版计算器
- javascript之-深入事件机制
- javascript事件捕获机制【深入分析IE和DOM中的事件模型】
- javascript中的事件捕获机制,深入理解并区别IE和DOM中的事件模型
- 【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)--转
- 深入分析J2ME平台MIDP低级事件处理机制
- 深入分析J2ME平台MIDP高级事件处理机制
- AJAX之旅(2):javascript中类的深入研究-实现和继承
- 深入分析J2ME平台MIDP高级事件处理机制
- 深入研究vs 2008中的javascript编辑调试器