uncategorized

JavaScript 事件冒泡和捕获

详解 Javascript 事件冒泡和事件捕获机制

背景

在浏览器开发的过程中,开发团队注意到一个问题,当为一个元素绑定一个事件的时候,那么当某些事件触发的时候,逻辑上不仅仅是该元素触发该事件。比如当点击某个元素的时候,由于元素和元素之间是可能重叠的,那么不仅仅是该元素被点击,重叠的元素其实也被点击了,所以事件流描述的就是页面元素接收事件的顺序,但是IENetScape 提出了完全相反的事件流顺序模型.

IE 提出的是事件冒泡流,也就是事件最开始由最具体的那个元素接收,然后逐级扩散到它的容器元素,就像泡泡一样从最底层冒到最外层,所以称为事件冒泡.

而 NetScape 提出的是事件捕获流,也就是说事件从最外层容器元素被逐级捕获,最终由最具体的那个元素捕获,所以称为事件捕获.

后来 w3c 采用折中的方式,制定了统一的标准: 先捕获再冒泡

eventflow image | 100%

上图表示,触发事件的目标元素是叶节点上的 <td>, 默认情况下会沿着绿色的线一级一级向上触发(假设它们都注册了事件监听器),在 addEventListener时,第三个参数可以指定不按绿色的线的方向来冒泡,而是按红色的线这样的方向来捕获

示例

DOM2级事件规定的为元素添加事件的 API 为:

target.addEventListener(type, listener[, useCapture])

其中前两个参数大家都比较熟悉,分别是事件类型和处理函数,最后一个可选参数就是为了指定事件在捕获阶段触发还是在冒泡阶段触发,默认为 false 表示在冒泡阶段触发

参考以下示例

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
28
29
30
31
32
33
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div style="background: red" id='three'>
3-----
<div style="background: chocolate" id='two'>
2-----
<div style="background: green" id='one'>1-----</div>
</div>
</div>
</body>
<script type="text/javascript">
let elem1 = document.querySelector('#one')
let elem2 = document.querySelector('#two')
let elem3 = document.querySelector('#three')
elem1.addEventListener('click', (e)=>{
console.log(1)
console.log(`event phase ${e.eventPhase}`)
}, false)
elem2.addEventListener('click', (e)=>{
console.log(2)
console.log(`event phase ${e.eventPhase}`)
}, true)
elem3.addEventListener('click', (e)=>{
console.log(3)
console.log(`event phase ${e.eventPhase}`)
}, false)
</script>
</html>

上面的代码构造了三个嵌套的元素,3在最外,1在最内,并为三个元素都指定了点击事件处理函数,其中1和3指定在冒泡阶段触发,而2指定在捕获阶段触发

参考上面的事件流示意图可以知道,三个事件处理函数的触发顺序为 2 -> 1 -> 3

因为捕获阶段先发生,所以2的事件会先被触发,其次到冒泡阶段的时候,因为1元素是目标元素,所以事件在事件流的第二个阶段触发(参照上图 Target Phase),然后才是3元素的事件在冒泡阶段触发

在事件处理函数中也可通过e.eventPhase输出当前的事件阶段进行查看:上述代码对应输出为:

1
2
3
4
5
6
7
2
event phase 1
1
event phase 2
3
event phase 3

其中 eventPhase 对应的数字代表的含义分别为:

eventPhase meaning
0 当前没有事件被处理
1 处于事件捕获阶段
2 处于正在目标阶段
3 处于事件冒泡阶段

应用

利用事件流的特性,我们可以实现为很多元素添加统一的事件处理函数:事件代理

1
2
3
4
5
6
7
8
<ul id='list'>
<li>#1</li>
<li>#2</li>
<li>#3</li>
<span>Stranger</span>
<li>#4</li>
<li>#5</li>
</ul>

当我们需要为每一个li元素添加点击事件,输出当前被点击的元素的 text 的时候,直观的做法就是为每一个元素逐个 addEventListener. 但这么做着实低效,利用事件代理,我们可以只为 ul 元素添加事件处理函数,因为根据事件流的思想,当我们点击 li 元素的时候,其实 ul 元素也会被点击,假设事件处理函数定义在冒泡阶段,那么对 li 的点击事件最终会冒泡到 ul 上,此时我们再进行对该元素的逻辑处理即可,参考:

1
2
3
4
5
6
7
8
9
10
11
<script>
let list = document.querySelector('#list')
list.addEventListener('click', (e)=>{
if (e.target.tagName === 'LI'){
console.log(e.target.innerText)
}
if (e.target.tagName === 'SPAN'){
console.log('You are a SPAN')
}
})
</script>

注*: 此处 tagName 在 HTML 文档里会被转为大写,而在XHTML或者其他XML格式中,tag原本名称的大小写会被保留不变

如以上代码所示,我们只需要一个处理函数就可以处理所有的 li 的点击事件,对于这样的比较机械化的事件使用事件代理可以大大简化代码,并且如果动态添加了新的元素,或者硬编码新的元素,我们也不需要动现有的处理逻辑,实现了自动扩展.

◉ End.


参考资料:

  1. 《javascript 高级程序设计》

  2. Spec - Event-flow

  3. MDN - EventPhase

如博文有叙述不妥以及不准确的地方, 望各位看客不吝赐教, 感谢.

Share