理解DOM中的事件流的概念


什么是事件流

:用术语说流是对输入输出设备的抽象。以程序的角度说,流是具有方向的数据。

事件流:从页面中接收事件的顺序。也就是说当一个事件产生时,这个事件的传播过程,就是事件流。

事件:用户或者浏览器自身执行的某个动作,比如load,click,mousemove等

事件处理程序:相应处理某个事件的函数叫做事件处理函数(也叫做事件侦听器)

比如说React中的单向数据流,Node中的流,又或是今天本文所讲的DOM事件流。都是流的一种生动体现。


理解DOM中的事件流

当浏览器发展到第四代时(IE4和Netscape Communicator 4),浏览器团队遇到一个很有意思的问题:页面的哪一部分会拥有特定的事件?想象下在一张纸上有一组同心圆,如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是一组圆。两家公司的开发团队在看待浏览器事件方面还是一致的。如果你单击了某个按钮,那么同时你也单击了按钮的容器元素,甚至整个页面。
事件流描述的是从页面中接受事件的顺序。但有意思的是,IE和Netscape开发团队居然提出了两个截然相反的事件流概念。IE的事件流是事件冒泡流,而Netscape的事件流是事件捕获流。


IE提出的事件冒泡

事件冒泡即事件开始时,由最具体的元素接收(也就是事件发生所在的节点),然后逐级传播到较为不具体的节点。

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
27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<button id="click">点击</button>
<script>
(function(){
var btn = document.getElementById("click");
btn.onclick = function(){
console.log("1. button");
}
document.body.onclick = function(){
console.log("2. document.body");
}
document.onclick = function(){
console.log("3. document");
}
window.onclick = function(){
console.log("4. window");
}
})()
</script>
</body>
</html>

在代码所示的页面中,如果点击了button,那么这个点击事件会得到如下的结果:
代码结果截图
也就是说,click事件首先在button元素上发生,然后逐级向上传播。这就是事件冒泡。


netscape提出的事件捕获

事件捕获的概念,与事件冒泡正好相反。它认为当某个事件发生时,父元素应该更早接收到事件,具体元素则最后接收到事件。比如说刚才的demo,如果是事件捕获的话,事件发生顺序会是刚好与上面相反的。即window,document,document.body,button。

虽然事件捕获是Netscape唯一支持的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也都支持这种事件流模型。但由于老版本的浏览器不支持,因此很少有人使用事件捕获。

所以放心的使用事件冒泡,有特殊需要再使用事件捕获即可。


DOM事件流

DOM事件流可以分为下面3个阶段:

  1. 事件捕获阶段
  2. 处于目标阶段
  3. 事件冒泡阶段
    dom事件

事件捕获阶段

也就是说,当事件发生时,首先发生的是事件捕获,为父元素截获事件提供了机会。
例如,我把上面的Demo中,window点击事件更改为使用事件捕获模式。

addEventListener最后一个参数,为true则代表使用事件捕获模式,false则表示使用事件冒泡模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
(function(){
var btn = document.getElementById("click");
btn.addEventListener("click",function(){
console.log("1. button");
},true)
//省略document.body和document
.....
window.addEventListener("click",function(){
console.log("4. window");
},true)
})()
</script>

结果如下:

可以看到,点击事件先被父元素截获了,且该函数只在事件捕获阶段起作用。

在DOM事件流中,事件的目标在捕获阶段不会接受到事件。这意味着在捕获阶段,事件从document到body后就定停止了。下一个阶段是处于目标阶段,于是事件在button上发生,并在事件处理中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回document。

但是:我们的各大浏览器总是不喜欢按照规范来,IE9,Safari,chrome,firefox及其更高的版本中都会在捕获阶段出发事件对象上的事件,最后导致有两个机会在目标对象上操作事件。


处于目标与事件冒泡阶段

事件到了具体元素时,在具体元素上发生,并且被看成冒泡阶段的一部分。
随后,冒泡阶段发生,事件开始冒泡。


阻止事件冒泡

件冒泡过程,是可以被阻止的。防止事件冒泡而带来不必要的错误和困扰。

这个方法就是:stopPropagation()

1
2
3
4
5
6
7
8
9
10
11
12
13
(function(){
var btn = document.getElementById("click");
btn.addEventListener("click",function(event){
console.log("1. button");
event.stopPropagation();
console.log('Stop Propagation!');
},false)
//省略document.body和document
.....
window.addEventListener("click",function(){
console.log("4. window");
},false)
})()

最后结果是:1.button,Stop Propagation!。通过stopPropagation();阻止了事件的冒泡。


事件处理程序类别

刚刚我们已经讲了事件处理程序就是相应处理某个事假的函数。它可以分为几个类别:

html事件处理程序

某个元素支持的某个事件可以用与事件处理程序同名的html特性来指定,该特性的值是能够执行的javascript代码,这也是我们最初学js,最开始的方法。

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

优点:简单明了,省去获取元素等一系列前提操作

缺点:html代码与js代码高度耦合,不符合分离原则


DOM0级别事件处理函数

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>


DOM2级事件处理程序

DOM2级添加了addEventListener(添加事件处理程序)和removeEventListener(移除事件处理程序),也就是我们刚刚讲的上面的DOM2例子。

添加事件处理函数addEventListener

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

移除事件处理函数removeEventListener

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

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);
};
function HelloWorld(){
alert('HellowWorld');
}
oBtn.addEventListener("click",showId,false);
oBtn.addEventListener("click",HelloWorld,false);
oBtn.removeEventListener('click',showId,false)
</script>

最后只能弹出HellowWorld

IE事件处理程序attachEvent,detachEvent

ie实现了与dom类似的两个方法,attachEvent(添加),detachEvent(删除)

1
2
3
 oBtn.attachEvent('onclick',showId);//这时候会报错,因为这里的是在window的作用域内
//修改如下
oBtn.detachEvent('onclick',showId) ;//点击没有任何反应