DOM的事件传递机制 —— 捕获与冒泡
在用react写form表单的时候,看到代码里面有一句e.preventdafault(),因为不知道这个函数什么意思,就查了一下,才发现和Event事件有关系,并且也逃不掉事件的捕获与冒泡。为此,借此机会顺便了解学习了一些相关知识。
文章参考:
- https://blog.techbridge.cc/2017/07/15/javascript-event-propagation/
- https://yeungkc.com/2019/06/14/native-js-event/#preventDefault
Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<ul id="list">
<li id="list-item">
<a id="list-item-link" target="_blank" href="http://google.com">
google.com
</a>
</li>
</ul>
</body>
<script type="text/javascript" src="../JavaScript/DOMEvent.js"></script>
</html>
其DOM树基本结构如下:
在Event中,一个有三个阶段(Phase),分别是捕获、目标和冒泡。
而在Event的参数当中,也有参数eventPhase
是用来专门表示这三个事件的,通过这个参数我们就可以知道事件处于哪个阶段了。
// Webside:
// https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-interface
// PhaseType
const unsigned short CAPTURING_PHASE = 1;
const unsigned short AT_TARGET = 2;
const unsigned short BUBBLING_PHASE = 3;
DOM在事件传递中,可以概括为三个阶段:
- 首先是从根节点出发,到达目标节点,若此过程中触发了事件,则被称为捕获(CAPTURING)事件;
- 当节点到达定义点击事件的目标节点时,此时则处于目标(TARGET)阶段。
- 而在目标节点触发事件后,最后就会一路往上回到根节点,此时若有事件发生,则是冒泡(BUBBLING)事件。
附(W3C讲Event flow的图):
小总结:
-
先捕获后冒泡
-
如何决定当前处于捕获阶段还是冒泡阶段?
通过
addEventListener
这个函数定义事件的类型和监听阶段。target.addEventListener(type, listener[, useCapture]);
其中,这个函数的第三个参数就是用来表示监听的阶段, 若
true
则表示处于捕获阶段;false
表示冒泡阶段。默认是冒泡阶段。
实际案例
// DOMEvent.js
const get = (id) => document.getElementById(id);
const list = get('list')
const listItem = get('list-item')
const listItemLink = get('list-item-link')
// list的捕获
list.addEventListener('click', (e) => {
console.log('我是list的捕获事件')
}, true)
// list的冒泡
list.addEventListener('click', (e) => {
console.log('我是list的冒泡事件')
}, false)
// listItem的捕获
listItem.addEventListener('click', (e) => {
console.log('我是listItem的捕获事件')
}, true)
// listItem的冒泡
listItem.addEventListener('click', (e) => {
console.log('我是listItem的冒泡事件')
}, false)
// listItemLink的捕获
listItemLink.addEventListener('click', (e) => {
console.log('我是listItemLink的捕获事件')
}, true)
// listItemLink的冒泡
listItemLink.addEventListener('click', (e) => {
console.log('我是listItemLink的冒泡事件')
}, false)
通过Chrome开发者工具,可以看到结果如下:
DOMEvent.js:16 我是list的捕获事件,phase value: 1
DOMEvent.js:26 我是listItem的捕获事件,phase value: 1
DOMEvent.js:36 我是listItemLink的捕获事件,phase value: 2
DOMEvent.js:41 我是listItemLink的冒泡事件,phase value: 2
DOMEvent.js:31 我是listItem的冒泡事件,phase value: 3
DOMEvent.js:21 我是list的冒泡事件,phase value: 3
由此可以看出和刚刚的结论是符合的,先进行捕获事件,随后到达target,最后就从下往上走。但这里需要的是:
在目标事件这里,不管addEventListenter
的第三个参数是true还是false,都没有先捕获后冒泡的说法,既这里会按顺序执行。
下面看看我们将顺序倒转之后代码和结果:
// DOMEvent.js
const get = (id) => document.getElementById(id);
const list = get('list')
const listItem = get('list-item')
const listItemLink = get('list-item-link')
// list的冒泡
list.addEventListener('click', (e) => {
console.log('我是list的冒泡事件,phase value:', e.eventPhase)
}, false)
// list的捕获
list.addEventListener('click', (e) => {
console.log('我是list的捕获事件,phase value:', e.eventPhase)
}, true)
// listItem的冒泡
listItem.addEventListener('click', (e) => {
console.log('我是listItem的冒泡事件,phase value:', e.eventPhase)
}, false)
// listItem的捕获
listItem.addEventListener('click', (e) => {
console.log('我是listItem的捕获事件,phase value:', e.eventPhase)
}, true)
// listItemLink的冒泡
listItemLink.addEventListener('click', (e) => {
console.log('我是listItemLink的冒泡事件,phase value:', e.eventPhase)
}, false)
// listItemLink的捕获
listItemLink.addEventListener('click', (e) => {
console.log('我是listItemLink的捕获事件,phase value:', e.eventPhase)
}, true)
结果:
DOMEvent.js:21 我是list的捕获事件,phase value: 1
DOMEvent.js:31 我是listItem的捕获事件,phase value: 1
DOMEvent.js:36 我是listItemLink的冒泡事件,phase value: 2
DOMEvent.js:41 我是listItemLink的捕获事件,phase value: 2
DOMEvent.js:26 我是listItem的冒泡事件,phase value: 3
DOMEvent.js:16 我是list的冒泡事件,phase value: 3
可以看到listItemLink部分会按照代码顺序执行。
因此,最后得出我们结论
- 先捕获,后冒泡
- 当事件传递到target时,无捕获冒泡之分
stopPropagation、prenventDefault、return false
stopPropagation()
当你想要取消事件传递时,就可以选择使用这个函数。函数加在哪里,事件就会在哪里停止传递。
const get = (id) => document.getElementById(id);
const list = get('list')
const listItem = get('list-item')
const listItemLink = get('list-item-link')
// list的捕获
list.addEventListener('click', (e) => {
console.log('我是list的捕获事件,phase value:', e.eventPhase)
}, true)
// list的冒泡
list.addEventListener('click', (e) => {
console.log('我是list的冒泡事件,phase value:', e.eventPhase)
}, false)
// listItem的捕获
listItem.addEventListener('click', (e) => {
// e.stopPropagation()
console.log('我是listItem的捕获事件,phase value:', e.eventPhase)
}, true)
// listItem的冒泡
listItem.addEventListener('click', (e) => {
// e.preventDefault() // 链接不跳转
e.stopPropagation() // 跳转还在,但不进行冒泡了
console.log('我是listItem的冒泡事件,phase value:', e.eventPhase)
}, false)
// listItemLink的捕获
listItemLink.addEventListener('click', (e) => {
console.log('我是listItemLink的捕获事件,phase value:', e.eventPhase)
}, true)
// listItemLink的冒泡
listItemLink.addEventListener('click', (e) => {
console.log('我是listItemLink的冒泡事件,phase value:', e.eventPhase)
}, false)
结果如下:
DOMEvent.js:16 我是list的捕获事件,phase value: 1
DOMEvent.js:27 我是listItem的捕获事件,phase value: 1
DOMEvent.js:39 我是listItemLink的捕获事件,phase value: 2
DOMEvent.js:44 我是listItemLink的冒泡事件,phase value: 2
DOMEvent.js:34 我是listItem的冒泡事件,phase value: 3
可以看到,当我在listItem的冒泡过程调用了这个函数之后,事件就没有再继续传递下去了。
但是这里需要注意,这里说的事件传递被停止,是指事件不会被传递到下一个节点,若在同一个节点中不止一个listener,还是会被执行的。
例如:在上述代码中添加这段代码:
// listItem 冒泡事件2
listItem.addEventListener('click', (e) => {
console.log('我是listItem的冒泡事件2 ,phase value:', e.eventPhase)
})
最终结果则是:
DOMEvent.js:16 我是list的捕获事件,phase value: 1
DOMEvent.js:27 我是listItem的捕获事件,phase value: 1
DOMEvent.js:44 我是listItemLink的捕获事件,phase value: 2
DOMEvent.js:49 我是listItemLink的冒泡事件,phase value: 2
DOMEvent.js:34 我是listItem的冒泡事件,phase value: 3
DOMEvent.js:39 我是listItem的冒泡事件2 ,phase value: 3
可以看到,另一个listener的监听事件还是会被正常执行。
假如你需要同一层级的其它listener也不会被执行,可以改用e.stopImmediatePropagation();
这样,其余listener也会被禁止。
prenventDefault()
它的作用是取消浏览器的预设行为,但是并不会对事件传递造成影响。但是一旦开始了prebentDefault()函数,在它往下传递的事件当中也会有效果。
例如:在listItem中添加prenventDefault(),可以看到此时a链接已经不能正常跳转了,但是,此时Chrome的输出栏里仍然可以看到这样的结果:
DOMEvent.js:16 我是list的捕获事件,phase value: 1
DOMEvent.js:27 我是listItem的捕获事件,phase value: 1
DOMEvent.js:44 我是listItemLink的捕获事件,phase value: 2
DOMEvent.js:49 我是listItemLink的冒泡事件,phase value: 2
DOMEvent.js:34 我是listItem的冒泡事件,phase value: 3
DOMEvent.js:21 我是list的冒泡事件,phase value: 3
但是这里我有一个疑问 —— 就是我是在listItem的冒泡阶段停止这个事件的,为什么还是不能跳转呢?
按理 #list (捕获) -> #listItem(捕获) -> listItemLink(捕获) -> listItemLink(冒泡) -> listItem(冒泡) -> list(冒泡),按这样的顺序进行,那么不应该也可以进行跳转吗?
return false
它可以看作前面两个函数的综合,当你调用它时,它既可以阻止传递同时还可以阻止默认行为。它在内部实际进行了三件事:
- event.preventDefault();
- event.preventDefault();
- 停止回调函数执行并立即返回。