您的位置:首页 > Web前端 > JavaScript

深入研究JavaScript的事件机制

2014-10-23 09:01 369 查看
本篇开始将回顾下Javascript的事件机制。同时会从一个最小的函数开始写到最后一个具有完整功能的,强大的事件模块。为叙述方便将响应函数/回调函数/事件Listener/事件handler都称为事件handler。

先看看页面中添加事件的几种方式:

直接将JS代码写在HTML上

测试:Nowamagic

viewsource

print?

1
<divonclick=
"alert('欢迎访问Nowamagic.net');"
>Nowamagic</div>
HTMLElement元素自身就拥有了很多onXXX属性,只需将JS代码赋值给其就可以了。赋值给onXXX的字符串将作为响应函数的函数体(FunctionBody)。大概这是上世纪90年代的写法,那时候直接把JS代码写在网页中很普遍,也许那时候的JS并不太重要,只是用来做做验证或一些花哨的效果而已。

定义一个函数,赋值给html元素的onXXX属性

viewsource

print?

1
<scripttype=
"text/javascript"
>
2
function
clk(){}
3
</script>
4
<divonclick=
"clk()"
>Div2Element</div>
先定义函数clk,然后赋值给onclick属性,这种方式也应该属于上世纪90年代的流行写法。比第一种方式好的是它把业务逻辑代码都封装在一个函数里了,使HTML代码与JS代码稍微有点儿分离,不至于第一种那么紧密耦合。

使用element.onXXX方式

viewsource

print?

1
<divid=
"d3"
>Div3Element</div>
2
<scripttype=
"text/javascript"
>
3
var
d3=document.getElementById(
'd3'
);
4
d3.onclick=
function
(){}
5
</script>
这种方式也是早期的写法,但好处是可以将JS与HTML完全分离,前提是需要给HTML元素提供一个额外的id属性(或其它能获取该元素对象的方式)。

使用addEventListener或attachEvent

viewsource

print?

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>
这是目前推荐的方式,较前两种方式功能更为强大,可以为元素添加多个事件handler,支持事件冒泡或捕获,前三种方式默认都是冒泡。IE6/7/8仍然没有遵循标准而使用了自己专有的attachEvent,且不支持事件捕获。

好,把方式4简单的封装下,兼容标准浏览器及IE浏览器。注意attachEvent的第一个参数需要加上个"on",addEventListener第三个参数为false表示事件冒泡,attachEvent没有第三个参数,默认就是冒泡,没有捕获。

viewsource

print?

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
}
好,用这个工具函数添加一个给document添加一个点击事件:

viewsource

print?

1
function
handler(){
2
alert(
this
);
3
alert(arguments[0]);
4
}
5
addEvent(document,
'click'
,handler);
在Firefox等标准浏览器中,点击页面后将弹出"[objectHTMLDocument]",及handler中的this就是document自身。但在IE6/7/8中this却是window对象。这让人不爽,修改下与标准浏览器统一。

viewsource

print?

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
}
上面我们封装了一个addEvent,解决了IE6/7/8下事件handler中this为window的错误,并且统一了事件对象作为事件handler的第一个参数传入。

这篇把对应的删除事件的函数补上。上一篇中fn在IE6/7/8中实际上被包装了,IE6/7/8中真正的handler是el["e"+fn]。因此删除时要用到它。同时将两个方法挂在一个对象E上,add,remove分别添加和删除事件。

viewsource

print?

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
};
可以看到,标准浏览器如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?

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下会无序,如

viewsource

print?

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>
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?

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
}();
上面解决了IE6/7/8中同一个类型事件的多个handler执行无序的情况,为此改动也是较大的。实现几乎与前一个版本完全不同。但好处也是明显的。

有时需要添加只执行一次的事件handler,为此给add方法添加第四个参数one,one为true则该事件handler只执行一次。

viewsource

print?

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>
再扩展下remove函数。

删除元素type类型的所有监听器(参数传el,type)

删除元素所有的监听器(仅传el)

比如当给一个el添加了3个click事件的handler,1个mouseover事件的handler

viewsource

print?

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);
使用以下语句将删除元素click的所有handler:E.remove(el,'click');

以下将删除元素身上所有的事件handler,包括click和mouseover:E.remove(el);

上面正式推出了我的事件模块event_v1,已经搭起了它的初始框架。或许有人要说,与众多JS库或框架相比,它还没有解决事件对象的兼容性问题。是的,我故意将此放到后续补充。因为事件对象的兼容性问题太多了,太繁琐了。

下面我将引入一个私有的_fixEvent函数,add中将调用该函数。_fixEvent将修复(或称包装)原生事件对象,返回一个标准的统一接口的事件对象。如下

viewsource

print?

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>

更多的差异性,不在这一一列举了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: