DOM的事件传递机制 —— 捕获与冒泡

DOM的事件传递机制 —— 捕获与冒泡

在用react写form表单的时候,看到代码里面有一句e.preventdafault(),因为不知道这个函数什么意思,就查了一下,才发现和Event事件有关系,并且也逃不掉事件的捕获与冒泡。为此,借此机会顺便了解学习了一些相关知识。

文章参考:

  1. https://blog.techbridge.cc/2017/07/15/javascript-event-propagation/
  2. 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树基本结构如下:

avatar

在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的图):

avatar

小总结:

  • 先捕获后冒泡

  • 如何决定当前处于捕获阶段还是冒泡阶段?

    通过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();
  • 停止回调函数执行并立即返回。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值