Bootstrap

JavaScript 学习之旅:第三十七课 - 事件冒泡和捕获

欢迎来到 JavaScript 学习之旅 的第三十七课!在这一课中,我们将深入探讨 JavaScript 中的 事件冒泡事件捕获。这两个概念是理解事件传播机制的关键,掌握了它们,你就可以更好地控制事件的触发顺序,避免不必要的事件触发,甚至优化网页的性能。让我们一起揭开这个神秘的面纱吧!


引言:为什么需要了解事件冒泡和捕获?

想象一下,你正在编写一个魔法程序,程序中有一个按钮,当你点击按钮时,它会弹出一个对话框,显示“你好,世界!”但是,如果你点击的是按钮内部的一个小图标呢?你会希望这个小图标也有自己的点击事件,但同时又不希望它触发按钮的点击事件。这时,事件冒泡和捕获就派上用场了!通过理解它们的工作原理,你可以精确地控制事件的传播,实现更复杂的交互效果。

幽默小贴士:

事件冒泡和捕获就像是魔法师的“魔法传递链”,当用户做出某个动作时,事件会在元素之间传递,就像魔法能量在不同的魔法物品之间流动一样! 🌟✨


1. 事件冒泡和捕获 介绍

在 JavaScript 中,事件的传播过程可以分为两个阶段:事件捕获事件冒泡。这两者都是事件从一个元素传递到另一个元素的方式,但它们的方向不同。

  • 事件捕获:事件从最外层的元素(如 documentwindow)开始,逐层向下传递,直到到达触发事件的目标元素。
  • 事件冒泡:事件从目标元素开始,逐层向上传递,直到到达最外层的元素(如 documentwindow)。
1.1 事件传播的三个阶段

事件的传播过程可以分为三个阶段:

  1. 捕获阶段:事件从最外层的元素(如 documentwindow)开始,逐层向下传递,直到到达触发事件的目标元素。
  2. 目标阶段:事件到达触发事件的目标元素,执行该元素上的事件处理函数。
  3. 冒泡阶段:事件从目标元素开始,逐层向上传递,直到到达最外层的元素(如 documentwindow)。
图解事件传播过程
          document
            /   \
           /     \
        body      (其他元素)
         /  \
        /    \
    div#outer  (其他元素)
         |
    div#inner  (目标元素)
  • 捕获阶段:事件从 document 开始,依次经过 bodydiv#outer,最终到达 div#inner
  • 目标阶段:事件在 div#inner 上触发。
  • 冒泡阶段:事件从 div#inner 开始,依次经过 div#outerbody,最终到达 document

2. 事件冒泡

事件冒泡 是指事件从目标元素开始,逐层向上传递,直到到达最外层的元素(如 documentwindow)。这是最常见的事件传播方式,默认情况下,大多数事件都会发生冒泡。

2.1 事件冒泡的示例

假设你有以下 HTML 结构,其中 div#inner 是嵌套在 div#outer 内部的。当用户点击 div#inner 时,事件会首先在 div#inner 上触发,然后依次冒泡到 div#outerbody,最后到达 document

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件冒泡示例</title>
    <style>
        #outer {
            width: 200px;
            height: 200px;
            background-color: lightblue;
            padding: 10px;
        }
        #inner {
            width: 100px;
            height: 100px;
            background-color: lightcoral;
        }
    </style>
</head>
<body>
    <div id="outer">
        外层盒子
        <div id="inner">内层盒子</div>
    </div>

    <script>
        // 获取元素
        const outer = document.getElementById("outer");
        const inner = document.getElementById("inner");

        // 为外层盒子添加点击事件监听器
        outer.addEventListener("click", () => {
            console.log("你点击了外层盒子");
        });

        // 为内层盒子添加点击事件监听器
        inner.addEventListener("click", () => {
            console.log("你点击了内层盒子");
        });
    </script>
</body>
</html>
代码解释:
  • 当用户点击 div#inner 时,事件会首先在 div#inner 上触发,输出 "你点击了内层盒子"
  • 然后,事件会冒泡到 div#outer,输出 "你点击了外层盒子"
  • 最后,事件会继续冒泡到 bodydocument,但由于我们没有为这些元素添加事件监听器,所以不会有任何输出。
2.2 阻止事件冒泡

有时你可能不希望事件冒泡到父元素。例如,当你点击一个按钮时,你不希望它触发外层容器的点击事件。这时,你可以使用 event.stopPropagation() 方法来阻止事件冒泡。

示例:阻止事件冒泡
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>阻止事件冒泡</title>
    <style>
        #outer {
            width: 200px;
            height: 200px;
            background-color: lightblue;
            padding: 10px;
        }
        #inner {
            width: 100px;
            height: 100px;
            background-color: lightcoral;
        }
    </style>
</head>
<body>
    <div id="outer">
        外层盒子
        <div id="inner">内层盒子</div>
    </div>

    <script>
        // 获取元素
        const outer = document.getElementById("outer");
        const inner = document.getElementById("inner");

        // 为外层盒子添加点击事件监听器
        outer.addEventListener("click", () => {
            console.log("你点击了外层盒子");
        });

        // 为内层盒子添加点击事件监听器,并阻止事件冒泡
        inner.addEventListener("click", (event) => {
            console.log("你点击了内层盒子");
            event.stopPropagation();  // 阻止事件冒泡
        });
    </script>
</body>
</html>
代码解释:
  • 当用户点击 div#inner 时,事件会首先在 div#inner 上触发,输出 "你点击了内层盒子"
  • 由于我们在 div#inner 的事件处理函数中调用了 event.stopPropagation(),事件不会继续冒泡到 div#outer,因此不会输出 "你点击了外层盒子"

3. 事件捕获

事件捕获 是指事件从最外层的元素(如 documentwindow)开始,逐层向下传递,直到到达触发事件的目标元素。默认情况下,事件捕获阶段不会触发任何事件处理函数,除非你明确指定了要在这个阶段处理事件。

3.1 使用 useCapture 参数

你可以通过在 addEventListener() 方法中传递第三个参数 useCapture 来指定是否在捕获阶段处理事件。如果 useCapture 设置为 true,则事件会在捕获阶段触发;如果设置为 false(默认值),则事件会在冒泡阶段触发。

示例:在捕获阶段处理事件
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>事件捕获示例</title>
    <style>
        #outer {
            width: 200px;
            height: 200px;
            background-color: lightblue;
            padding: 10px;
        }
        #inner {
            width: 100px;
            height: 100px;
            background-color: lightcoral;
        }
    </style>
</head>
<body>
    <div id="outer">
        外层盒子
        <div id="inner">内层盒子</div>
    </div>

    <script>
        // 获取元素
        const outer = document.getElementById("outer");
        const inner = document.getElementById("inner");

        // 为外层盒子添加点击事件监听器,在捕获阶段处理事件
        outer.addEventListener("click", () => {
            console.log("你点击了外层盒子(捕获阶段)");
        }, true);  // 第三个参数为 true,表示在捕获阶段处理事件

        // 为内层盒子添加点击事件监听器,在冒泡阶段处理事件
        inner.addEventListener("click", () => {
            console.log("你点击了内层盒子(冒泡阶段)");
        }, false);  // 第三个参数为 false,表示在冒泡阶段处理事件
    </script>
</body>
</html>
代码解释:
  • 当用户点击 div#inner 时,事件会首先在捕获阶段触发,输出 "你点击了外层盒子(捕获阶段)"
  • 然后,事件会进入目标阶段,在 div#inner 上触发,输出 "你点击了内层盒子(冒泡阶段)"
  • 最后,事件会继续冒泡到 div#outer,但由于我们没有为 div#outer 添加冒泡阶段的事件监听器,所以不会有任何输出。
3.2 捕获阶段 vs 冒泡阶段
  • 捕获阶段:事件从最外层的元素开始,逐层向下传递,直到到达目标元素。如果你想在事件到达目标元素之前处理它,可以使用捕获阶段。
  • 冒泡阶段:事件从目标元素开始,逐层向上传递,直到到达最外层的元素。这是最常见的事件传播方式,默认情况下,大多数事件都会发生冒泡。

4. 事件传播阶段

事件的传播过程可以分为三个阶段:

  1. 捕获阶段:事件从最外层的元素(如 documentwindow)开始,逐层向下传递,直到到达触发事件的目标元素。
  2. 目标阶段:事件到达触发事件的目标元素,执行该元素上的事件处理函数。
  3. 冒泡阶段:事件从目标元素开始,逐层向上传递,直到到达最外层的元素(如 documentwindow)。
小贴士:

你可以通过在 addEventListener() 方法中传递第三个参数 useCapture 来指定事件是在捕获阶段还是冒泡阶段处理。默认情况下,事件会在冒泡阶段处理。


5. 阻止事件冒泡和默认行为

除了阻止事件冒泡,你还可以阻止事件的默认行为。例如,点击链接时会跳转到另一个页面,提交表单时会刷新页面。如果你不希望这些默认行为发生,可以使用 event.preventDefault() 方法来阻止它们。

5.1 阻止事件冒泡

我们已经在前面的示例中介绍了如何使用 event.stopPropagation() 来阻止事件冒泡。

5.2 阻止默认行为
示例:阻止链接的默认行为
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>阻止默认行为</title>
</head>
<body>
    <h1>欢迎来到我的网页</h1>
    <a href="https://example.com" id="magic-link">点击这里</a>

    <script>
        // 获取链接元素
        const magicLink = document.getElementById("magic-link");

        // 为链接添加点击事件监听器
        magicLink.addEventListener("click", (event) => {
            // 阻止链接的默认行为(跳转到 example.com)
            event.preventDefault();

            // 显示一条消息
            alert("你点击了链接,但不会跳转到其他页面!");
        });
    </script>
</body>
</html>
代码解释:
  • event.preventDefault(); 阻止链接的默认行为(即跳转到 https://example.com)。
  • alert("你点击了链接,但不会跳转到其他页面!"); 显示一条消息,告知用户链接的点击事件被阻止了。

6. 挑战:创建一个带有菜单的网页

挑战任务

假设你正在编写一个魔法程序,程序中有一个菜单,菜单中有多个子项。当用户点击某个子项时,应该只触发该子项的点击事件,而不触发父级菜单的点击事件。你需要使用事件冒泡和捕获的知识来实现这个功能。

挑战代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>带有菜单的网页</title>
    <style>
        .menu {
            border: 1px solid black;
            padding: 10px;
        }
        .menu-item {
            margin: 5px;
            padding: 5px;
            background-color: lightgray;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h1>欢迎来到我的网页</h1>
    <div class="menu">
        <p>这是一个菜单:</p>
        <div class="menu-item">菜单项 1</div>
        <div class="menu-item">菜单项 2</div>
        <div class="menu-item">菜单项 3</div>
    </div>

    <script>
        // 获取菜单和菜单项
        const menu = document.querySelector(".menu");
        const menuItems = document.querySelectorAll(".menu-item");

        // 为菜单添加点击事件监听器
        menu.addEventListener("click", () => {
            console.log("你点击了菜单");
        });

        // 为每个菜单项添加点击事件监听器,并阻止事件冒泡
        menuItems.forEach((item) => {
            item.addEventListener("click", (event) => {
                console.log(`你点击了菜单项 ${item.textContent}`);
                event.stopPropagation();  // 阻止事件冒泡
            });
        });
    </script>
</body>
</html>
代码解释:
  • const menu = document.querySelector(".menu"); 获取菜单元素。
  • const menuItems = document.querySelectorAll(".menu-item"); 获取所有菜单项。
  • menu.addEventListener("click", () => { ... }); 为菜单添加点击事件监听器,当用户点击菜单时,输出 "你点击了菜单"
  • menuItems.forEach((item) => { ... }); 为每个菜单项添加点击事件监听器,并阻止事件冒泡。当用户点击某个菜单项时,输出相应的菜单项内容,但不会触发菜单的点击事件。

7. 注意事项

  1. 不要滥用事件冒泡:虽然事件冒泡可以让事件在多个元素之间传递,但过多的事件冒泡可能会导致性能问题。尽量只在需要的地方使用事件冒泡,并在不再需要时及时阻止它。
  2. 注意事件捕获的优先级:事件捕获阶段的事件处理函数会比冒泡阶段的事件处理函数先执行。如果你在同一元素上同时设置了捕获阶段和冒泡阶段的事件处理函数,捕获阶段的处理函数会先触发。
  3. 避免过度使用 event.stopPropagation():虽然 event.stopPropagation() 可以阻止事件冒泡,但它也可能会导致其他依赖于事件冒泡的功能失效。因此,只有在确实需要阻止事件冒泡时才使用它。
  4. 处理键盘事件时要注意特殊字符:在处理键盘事件时,某些键(如 EnterShiftCtrl 等)可能会影响页面的行为。你可以使用 event.keyevent.code 来检查用户按下了哪个键,并根据需要进行处理。
幽默小贴士:

事件冒泡和捕获就像是魔法师的“魔法传递链”,当用户做出某个动作时,事件会在元素之间传递,就像魔法能量在不同的魔法物品之间流动一样!合理使用它们,可以让网页变得更加智能和互动! 🌟✨


本课总结

在这节课中,我们学习了以下内容:

  1. 事件冒泡和捕获介绍:了解了什么是事件冒泡和捕获,以及它们在事件传播中的作用。
  2. 事件冒泡:学会了如何使用事件冒泡让事件从目标元素逐层向上传递,并使用 event.stopPropagation() 阻止事件冒泡。
  3. 事件捕获:学会了如何使用 useCapture 参数在捕获阶段处理事件。
  4. 事件传播阶段:了解了事件传播的三个阶段:捕获阶段、目标阶段和冒泡阶段。
  5. 阻止事件冒泡和默认行为:学会了如何使用 event.stopPropagation() 阻止事件冒泡,以及如何使用 event.preventDefault() 阻止默认行为。
  6. 挑战:通过一个带有菜单的网页挑战,练习了如何使用事件冒泡和捕获实现复杂的交互效果。
  7. 注意事项:提醒你在使用事件冒泡和捕获时要注意的一些事项,如避免滥用、处理优先级等。

下次见!

恭喜你完成了第三十七课的学习!你现在已经掌握了事件冒泡和捕获的基本用法,能够更好地控制事件的传播,避免不必要的事件触发。如果你有任何问题或需要进一步的帮助,请随时回来找我们。我们将在未来的课程中继续为你提供更多有趣的内容。祝你在 JavaScript 的世界里一切顺利,成为真正的编程大师!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;