关于事件捕获与事件冒泡先后执行顺序

4,084 阅读8分钟

@关于事件捕获与事件冒泡先后执行顺序

create by db on 2021-2-21 14:33:31
Recently revised in 2022-7-20 17:38:36

闲时要有吃紧的心思,忙时要有悠闲的趣味

目录

前言

返回目录

I hear and I fogorget.

I see and I remember.

I do and I understand.

强势插入2022年7月20日

更新当前事件捕获与事件冒泡先后执行顺序为先捕获后冒泡

/强势插入2022年7月20日

  作为一名前端开发,DOM是我们最熟悉的伙伴之一——每天F11都能看到它。

  但是,你真的懂它吗?能描述一下事件捕获与事件冒泡先后执行顺序吗?

  如果一时想不起来,我们就去看一下吧。

正文

返回目录

DOM 事件流的三个阶段

  事件发生时会在元素节点之间按照特定的顺序传播,这个传播过程即DOM 事件流

  当一个 DOM 事件被触发时,它不仅仅只是单纯地在本身对象上触发一次,而是会经历三个不同的阶段:

1. 捕获阶段(Capture Phase):

  • 当我们在 DOM 树的某个节点发生了一些操作(例如单击、鼠标移动上去),就会有一个事件发射过去。这个事件从 Window 发出,不断经过下级节点直到触发的目标节点。在到达目标节点之前的过程,就是捕获阶段(Capture Phase)。(所有经过的节点,都会触发这个事件。捕获阶段的任务就是建立这个事件传递路线,以便后面冒泡阶段顺着这条路线返回 Window。)在目标元素对象本身上注册的捕获事件处理程序不会被调用。

2. 目标阶段(Target Phase):

  • 当事件不断的传递直到目标节点的时候,最终在目标节点上触发这个事件,就是目标阶段。

3. 冒泡阶段(Bubbling Phase):

  • 再从目标事件位置往文档的根节点方向回溯,从内向外冒泡事件对象(我们平时用的事件绑定就是利用的事件冒泡的原理)

如图所示:

  事件捕获与事件冒泡先后执行顺序就显而易见了。

DOM 元素绑定 js 事件方式:

onclick

  在 html 标签里面或通过赋值的方式创建 onclick 事件 ,重写 onclick 会覆盖之前的属性,只支持冒泡阶段,不存在兼容性问题

绑定事件: element.onclick = function(){}

解绑事件: element.onclick = null

addEventListener

  IE8 以下不支持,属于 DOM2 级方法,可以添加多个方法不被覆盖

参数说明:

  • event,必须。字符串,指定事件名。 不要使用 "on" 前缀。 例如,使用 "click" , 而不是使用 "onclick"。

  • function 必须。指定要事件触发时执行的函数,注意只写函数名,不要带括号。

  • useCapture 可选。布尔值,指定事件是否在捕获或冒泡阶段执行。

绑定事件:

element.addEventListener(
    'click',
    function(e) {
        e.preventDefault() //阻止默认事件
    },
    false
)

解绑事件:

element.removeEventListener('click',function(){},false)

attachEvent

  IE 特有,兼容 IE8 及以下版本,可添加多个事件处理程序,只支持冒泡阶段

参数说明:

  • event,必须。字符串,指定事件名。注意加上事件前边的“on”,比如“onclick”和“onmouseover”,这是与 addEventListener 的区别。
  • function 要绑定的事件监听函数,注意只写函数名,不要带括号。

绑定事件:

element.attachEvent('onclick', function(e) {
    e.returnValue = false //阻止默认事件
})

解绑事件: element.detachEvent("onclick",function(){})

上代码,做实验

  打开在线编辑器 ---> jsbin.com/cedorat/edi…

代码如下:

</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        #outer {
            text-align: center;
            width: 400px;
            height: 400px;
            background-color: yellow;
            margin: 0 auto;
        }

        #middle {
            width: 250px;
            height: 250px;
            background-color: red;
            margin: 0 auto;
        }

        #inner {
            width: 100px;
            height: 100px;
            background-color: green;
            margin: 0 auto;
        }
    </style>
</head>

<body>
    <div id='outer'>
        <span>outer</span>
        <div id='middle'>
            <span>middle</span>
            <div id='inner'>
                <span>inner</span>
            </div>
        </div>
    </div>
    <script>
        // 先全部绑定捕获事件再全部绑定冒泡事件
        on("outer", "click", o_click_c, true);
        on("middle", "click", m_click_c, true);
        on("inner", "click", i_click_c, true);

        on("outer", "click", o_click_b, false);
        on("middle", "click", m_click_b, false);
        on("inner", "click", i_click_b, false);

        // // 先全部绑定冒泡事件再全部绑定捕获事件
        // on("outer", "click", o_click_b, false);
        // on("middle", "click", m_click_b, false);
        // on("inner", "click", i_click_b, false);

        // on("outer", "click", o_click_c, true);
        // on("middle", "click", m_click_c, true);
        // on("inner", "click", i_click_c, true);

        // 挨个绑定捕获及冒泡事件
        // on("outer", "click", o_click_c, true);
        // on("outer", "click", o_click_b, false);

        // on("middle", "click", m_click_c, true);
        // on("middle", "click", m_click_b, false);

        // on("inner", "click", i_click_c, true);
        // on("inner", "click", i_click_b, false);

        // 挨个绑定冒泡及捕获事件
        // on("outer", "click", o_click_b, false);
        // on("outer", "click", o_click_c, true);

        // on("middle", "click", m_click_b, false);
        // on("middle", "click", m_click_c, true);

        // on("inner", "click", i_click_b, false);
        // on("inner", "click", i_click_c, true);

        // 选择相应元素
        function $(element) {
            return document.getElementById(element);
        }
        // 绑定方法
        function on(element, event_name, handler, use_capture) {
            if (addEventListener) { //所有主流浏览器,除了 IE 8 及更早 IE版本
                $(element).addEventListener(event_name, handler, use_capture);
            } else { // IE 8 及更早 IE 版本
                $(element).attachEvent('on' + event_name, handler);
            }
        }

        function o_click_c() {
            console.log("outer_捕获");
        }

        function m_click_c() {
            console.log("middle_捕获")
        }

        function i_click_c() {
            console.log("inner_捕获")
        }

        function o_click_b() {
            console.log("outer_冒泡")
        }

        function m_click_b() {
            console.log("middle_冒泡")
        }

        function i_click_b() {
            console.log("inner_冒泡")
        }
    </script>
</body>

</body>

</html>

  在上述代码中,我们定义了四种执行顺序:

  1. 先全部绑定捕获事件再全部绑定冒泡事件

  2. 先全部绑定冒泡事件再全部绑定捕获事件

  3. 挨个绑定捕获及冒泡事件

  4. 挨个绑定冒泡及捕获事件

一、先全部绑定捕获事件再全部绑定冒泡事件

我们先运行第一种:

  1. 点击 outer,打印结果:
"outer_捕获"
"outer_冒泡"
  1. 点击 middle,打印结果:
"outer_捕获"
"middle_捕获"
"middle_冒泡"
"outer_冒泡"
  1. 点击 inner,打印结果:
"outer_捕获"
"middle_捕获"
"inner_捕获"
"inner_冒泡"
"middle_冒泡"
"outer_冒泡"

结论:

  • 先由外向内事件捕获,一直到事发元素,再由内向外冒泡到根节点上

二、先全部绑定冒泡事件再全部绑定捕获事件

我们先运行第二种:

  1. 点击 outer,打印结果:
"outer_捕获"
"outer_冒泡"
  1. 点击 middle,打印结果:
"outer_捕获"
"middle_捕获"
"middle_冒泡"
"outer_冒泡"
  1. 点击 inner,打印结果:
"outer_捕获"
"middle_捕获"
"inner_捕获"
"inner_冒泡"
"middle_冒泡"
"outer_冒泡"

结论:

  • 先由外向内事件捕获,一直到事发元素,再由内向外冒泡到根节点上

三、挨个绑定捕获及冒泡事件

我们先运行第三种:

  1. 点击 outer,打印结果:
"outer_捕获"
"outer_冒泡"
  1. 点击 middle,打印结果:
"outer_捕获"
"middle_捕获"
"middle_冒泡"
"outer_冒泡"
  1. 点击 inner,打印结果:
"outer_捕获"
"middle_捕获"
"inner_捕获"
"inner_冒泡"
"middle_冒泡"
"outer_冒泡"

结论:

  • 先由外向内事件捕获,一直到事发元素,再由内向外冒泡到根节点上

四、挨个绑定冒泡及捕获事件

我们先运行第四种:

  1. 点击 outer,打印结果:
"outer_捕获"
"outer_冒泡"
  1. 点击 middle,打印结果:
"outer_捕获"
"middle_捕获"
"middle_冒泡"
"outer_冒泡"
  1. 点击 inner,打印结果:
"outer_捕获"
"middle_捕获"
"inner_捕获"
"inner_冒泡"
"middle_冒泡"
"outer_冒泡"

结论:

  • 先由外向内事件捕获,一直到事发元素,再由内向外冒泡到根节点上

阻止事件的传播

  如果希望事件到某个节点为止,不再传播,可以使用事件对象的 event.stopPropagation() 方法。

  注意:它不是阻止冒泡,而是阻止的事件的传播!!!事件的捕获和冒泡都会阻止掉!!!

// 阻止所有middle捕获之后的事件
document.getElementById("middle").addEventListener('click', function(e) {
    e.stopPropagation();
    console.log('阻止所有middle捕获之后的事件')
}, true);

  点击 inner,打印结果:

outer_捕获
middle_捕获
阻止所有middle捕获之后的事件

只阻止冒泡

  如果只希望防止事件冒泡,我们可以借助 addEventListener 的第三个参数,指定事件在冒泡阶段执行。

// 阻止所有middle冒泡之后的事件
document.getElementById("middle").addEventListener('click', function(e) {
    e.stopPropagation();
    console.log('阻止所有middle冒泡之后的事件')
}, true);

  点击 inner,打印结果:

outer_捕获
middle_捕获
inner_捕获
inner_冒泡
middle_冒泡
test.html:89 阻止所有middle捕获之后的事件

注意

  • stopPropagation 会阻止事件向上层元素冒泡。如果同一个元素绑定了多个事件(addEventListener),那么不会阻止其他事件的执行。

阻止剩余事件

event.stopImmediatePropagation() 方法阻止剩下的事件处理程序被执行。该方法阻止事件在 DOM 树中向上冒泡。停止当前节点,和所有后续节点的事件处理程序的运行。

// 阻止所有middle捕获之后的事件
document.getElementById("middle").addEventListener('click', function(e) {
    e.stopPropagation();
    // e.stopImmediatePropagation();
    console.log('阻止所有middle捕获之后的事件')
}, true);
// 绑定第二个事件
document.getElementById("middle").addEventListener('click', function(e) {
    console.log('绑定第二个捕获事件')
}, true);
// 绑定第二个事件
document.getElementById("middle").addEventListener('click', function(e) {
    console.log('绑定第三个捕获事件')
}, true);

  点击 inner,打印结果:

outer_捕获
middle_捕获
inner_捕获
inner_冒泡
middle_冒泡
test.html:89 阻止所有middle捕获之后的事件

总结

返回目录

事件流执行顺序

  通过以上代码,我们可以看出,关于事件捕获与事件冒泡先后执行顺序:

  1. 在捕获阶段,先由外向内执行捕获事件

  2. 当事件触发在目标阶段时,先捕获,后冒泡

  3. 在冒泡阶段,由内向外冒泡到根节点上

其他:

  1. js 代码只能执行捕获或者冒泡其中一个阶段(要么是捕获要么是冒泡)

  2. onclick 和 attachevent(ie)只能得到冒泡阶段

  3. 实际开发中,我们很少使用事件捕获,我们更关注事件冒泡

  4. 有些事件是没有冒泡的,比如 onblur、onfocus、onmouseenter、onmouseleave

  5. 事件的冒泡有时会带来麻烦,不过是可以被阻止的,方法是:stopPropagation()

  6. stopPropagation() 方法:终止事件在传播过程的捕获、目标处理或冒泡阶段进一步传播。调用该方法后,该节点上处理该事件的处理程序将被调用,事件不再被分派到其他节点。

  路漫漫其修远兮,与诸君共勉。

参考文档:

后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址

文档协议

db 的文档库db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。