事件流详解

第一次在segmentfault写文章,希望通过这种方式来巩固所学的知识,也欢迎童鞋们指正其中有不对和错误的地方。^+^

  1. 事件流

事件流:页面中接收事件的顺序,即当一个事件发生时,该事件的传播过程便叫做事件流

事件流的种类

事件冒泡

事件由最具体的元素开始逐级向上传播至较为不具体的节点(文档)

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="box">点我</div>
</body>
</html>

当我们点击id为box的div时,该点击事件传播顺序如下

div –> body –> html –>document

特别说明:IE5.5及更早的版本将绕过html节点直接到document,IE9,Firefox,chrome和safari将冒泡到window对象

事件捕获

事件捕获和事件冒泡似乎截然相反,由不太具体的节点先接收到事件 –>再到最具体的节点。同样还是用上面冒泡例子,则事件的传播顺序则是:

document –> html –>body –>div

特别说明:ie8之前不支持事件捕获,IE9,safari,chrome,opera,firefox目前支持良好。并且这些浏览器不是从document开始捕获,而是从window对象开始。

DOM事件流阶段

  1. 捕获阶段
  2. 目标阶段
  3. 冒泡阶段

事件流图解

以上面的代码为例子,由图可以很清晰地看出首先发生的是事件捕获–>实际的目标接收事件–>事件冒泡

特别说明:在DOM事件流中,实际的目标不会在捕获阶段接收到事件,即捕获阶段到body就停止,”下一阶段”是目标阶段,该阶段可以看成是事件冒泡的一部分,最终事件又被传播会document。
BUT :我们的各大浏览器总是不喜欢按照规范来,IE9,Safari,chrome,firefox及其更高的版本中都会在捕获阶段出发事件对象上的事件,最后导致有两个机会在目标对象上操作事件。

  1. ##事件处理程序
    事件:用户或者浏览器自身执行的某个动作,比如load,click,mousemove等
    事件处理程序:相应某个事件的函数叫做事件处理函数(也叫做事件侦听器

    事件处理程序类别

    1 html事件处理程序:某个元素支持的某个事件可以用与事件处理程序同名的html特性来指定,该特性的值是能够执行的javascript代码。
    1
    2
    3
    4
    5
    <input type="button" value="点击" onclick="alert('我被点击了')" />
    /*
    当点击该按钮的时候,浏览器会弹出'我被点击了';
    */

当然也可以给onclick赋值页面中其他地方定义的脚本

1
2
3
4
5
6
7
8
9
10
11
<script>
function show(){
alert('我被点击了');
}
/*
点击后也会弹出 '我被点击了'
*/
</script>
<input type="button" value="点击" onclick="show()" />

优点:简单明了,省去获取元素等一系列前提操作
缺点:html代码与js代码高度耦合,不符合分离原则


2 DOM0级别事件处理函数:使用 element.on[eventname]=fn的方式给元素添加事件

1
2
3
4
5
6
7
8
9
10
<input type="button" value="点击" id="click" />
<script>
var oBtn=document.getElementById('click');
//该方式被认为是元素的方法,即事件处理程序在元素的作用域中进行,this即该元素本身
oBtn.onclick=function(){
alert(this.id);//click
}
//注意:删除该事件处理程序可以用如下方法
oBtn.onclick=null;//即点击后不再有任何反应
</script>


3 DOM2级事件处理程序:DOM2级添加了addEventListener(添加事件处理程序)和removeEventListener(移除事件处理程序)
添加事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<input type="button" value="点击" id="click" />
<script>
var oBtn=document.getElementById('click');
/* 参数1 指定事件名称...click mouseover mouseout
参数2 事件处理程序(匿名函数或者有名函数)
参数3 true(捕获阶段发生) or false(冒泡阶段发生)
*/
oBtn.addEventListener('click',function(){
alert(this.id)//click this指的是该元素作用域内
},false)
//注意该种方式可以给一个函数添加多个事件处理函数,执行顺序与添加顺序相同
oBtn.addEventListener('click',function(){
alert('Hello World')//click
},false)
</script>

移除事件处理函数:如果事件处理函数是有名函数,则可以通过名字来移除,匿名函数无法移除。

1
2
3
4
5
6
7
8
9
10
11
12
<input type="button" value="点击" id="click" />
<script>
var oBtn=document.getElementById('click');
function showId(){
alert(this.id);
};
function HellowWorld(){
alert('HellowWorld');
}
oBtn.removeEventListener('click',showId,false)
//最后只能弹出HellowWorld
</script>

4IE事件处理程序:ie实现了与dom类似的两个方法,attachEvent(添加),detachEvent(删除)
添加事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
<input type="button" value="点击" id="click" />
<script>
var oBtn=document.getElementById('click');
function showId(){
alert(this.id);
};
oBtn.attachEvent('onclick',showId);//这时候会报错,因为这里的是在window
的作用域内
//修改如下
oBtn.attachEvent('onclick',function(){
showId.call(this);//改变this指向
})
</script>

删除事件处理函数

1
2
3
4
5
6
7
8
9
10
11
<input type="button" value="点击" id="click" />
<script>
var oBtn=document.getElementById('click');
function showId(){
alert('HelloWorld');
};
oBtn.attachEvent('onclick',showId);//这时候会报错,因为这里的是在window
的作用域内
//修改如下
oBtn.detachEvent('onclick',showId) ;//点击没有任何反应
</script>

事件函数封装

绑定 为了解决attachEvent的this指向问题,并且可以通过有名称的函数来解除事件绑定,现在处理如下

1
2
3
4
5
6
7
8
9
10
11
12
13
//
function bind(obj,eventName,fn){
var _fn=fn;
fn=function(){
_fn.call(obj);//改变this指向
};
if(obj.addEventListener){
obj.addEventListener(eventName,fn,false);
}else{
obj.attachEvent('on'+eventName,fn);
}
return fn;//用于事件解除
}

解除

1
2
3
4
5
6
7
function unbind(obj,eventName,fn){
if(obj.removeEventListener){
obj.removeEventListener(eventName,fn);
}else{
obj.detachEvent('on'+eventName,fn);
}
}

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//给input添加和移除事件
<input type="button" id="box" value="事件封装" />
//添加
function show( ){
alert(this);
}
function show2( ){
alert(this.id);
}
var removeFn=bind('box','click',show);//需要移除的事件处理程序,不是原程序名称show
bind('box','click',show2);
unbind('box','click',removeFn);
//最后只会弹出 box

事件对象

当触发DOM上面的某个事件的时候,会产生一个事件对象event,这个对象中包含着所有与事件对象有关的信息。例如该事件类型导致事件的元素

DOM中的事件对象

DOM中的事件对象:兼容DOM的浏览器会将event对象传入到事件处理程序中,无论指定事件处理程序用什么方式(html方式,DOM0级方式,DOM2级方式)

1
2
3
4
5
6
7
8
9
10
11
<input type="button" id="box" value="DOM中的事件对象" onclick="console.log(event.type)">
//html方法 click
var oBox=document.getElementById('box');
//DOM0级别方法 click
oBox.onclick=function(ev){
console.log(ev.type);//click
}
///DOM2级别方法 click
oBox.addEventListener('click',function(ev){
console.log(ev.type);//click
})

总的来说event对象包含与创建他的特定事件有关的属性和方法,但是触发的事件类型不同,则可用的属性和方法也不一样。但是都会包含以下成员

属性/方法 类型 读/写 说明
currentTarget element 只读 事件处理程序当前正在处理程序的那个元素,我的理解是事件的直接绑定者
target element 只读 事件的目标
cancelable boolean 只读 表示是否可以取消事件的默认行为
preventDefault() function 只读 取消事件的默认行为 ,前提是cancelable为true
bubbles boolean 只读 表明事件是否可以冒泡
stopPropagation() function 只读 取消事件的进一步冒泡或者捕获,前提是bubbles为true
type boolean 只读 事件类型
view abstractView 只读 与事件关联的抽象视图,等同于发生事件的window对象
detail integer 只读 与事件相关的细节信息
eventPhase integer 只读 调用事件处理程序的阶段,1::捕获,2:“处于目标”,3:冒泡
trusted boolean 只读 为true表示事件是由浏览器生成的,为false表示事件是由开发人员通过js生成的。(DOM3)
stopImmediatePropagation() function 只读 取消事件的进一步捕获或者冒泡,同时阻止任何事件处理程序被调用(DOM3)

特别说明:只有在事件处理程序被执行的期间,event对象才会存在,一旦事件处理程序执行完成,其就会被销毁。

IE中的事件对象

与访问DOM中的事件对象不同,要访问IE中的event对象有几种不同的方式。取决于指定事件处理程序的方法。

  1. html event
  2. 函数参数
  3. window.event

同样IE中的event对象也包含着与创建他的事件相关的属性和方法,其中很多的属性和方法都有对应的或者是相关的DOM属性和方法。当然也会事件的不同,其属性和方法也会有所不同,但是都会包含下表内容

属性/方法 类型 读/写 说明
srcElement element 只读 事件的目标(与DOM中的target属性相同)
type string 只读 事件的类型
cancelBubble boolean 只读 默认为false,设置为true表示取消冒泡(与stopPropagation()作用相同)
returnValue boolean 只读 默认为true,设置为false就可以取消默认行为(与preventDefault()作用相同)

跨浏览器事件对象封装

我们为eventUtil添加几个方法,以此来达到有关event对象的常用的跨浏览器的使用目标

  1. getEvent() 获取事件对象
  2. getTarget()获取事件源
  3. stopPropagation() 取消冒泡
  4. preventDefault() 阻止默认行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var eventUtil={
getEvent:function(ev){
return ev || window.event;//获取事件对象
},
getTarget:function(ev){
return ev.target || ev.srcElement;//获取事件源
},
stopPropagation:function(ev){//阻止冒泡
if(ev.stopPropagation){
ev.stopPropagation();
}else{
ev.cancelBubble=true;
}
},
preventDefault:function(ev){//阻止默认行为
if(ev.preventDefault){
ev.preventDefault();
}else{
ev.returnValue=true;
}
}
}

常见应用之事件委托

说明:需要给页面中成百上千个li绑定一个事件并且输出当前元素的innerHTML
常见做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
var aLi //假设已经获取了这组li元素
for(var i=0;i<aLi.length;i++){
aLi[i].onclick=function(ev){
console.log(this.innerHTML);
}
}

这种方式通过遍历DOM节点的方式添加事件处理程序有诸多缺点,比如性能大大减低,新添加的li不具备click事件等。

利用事件委托(冒泡原理)

1
2
3
4
5
6
var oUl//假设oUl是li的父节点
oUL.onclick=fuction(ev){
var ev=eventUtil.getEvent(ev);
var target=eventUtil.getTarget(ev);
console.log(target.innerHTML);
}

利用事件委托可以大大地提高性能,后面随时添加的元素都可以拥有这个点击事件等