事件冒泡、事件捕获和事件委托

161 阅读5分钟

事件冒泡、事件捕获和事件委托

今天小哆啦的舍友问了一个关于事件的问题,小哆啦发现自己不是特别清楚特别明白,然后小哆啦就恶补了一下知识点,今天咱们就一起来深入了解一下。

参考资料 :JavaScript - Event order (quirksmode.org)

首先先了解一下这几个概念

具体概念

事件冒泡(Event Bubbling): 事件冒泡是指当一个元素上的事件被触发后,事件会从该元素开始沿着 DOM 树向上冒泡到更高层次的父元素,直至达到根节点。这意味着如果一个子元素上的事件被触发,其父元素上绑定的相同事件也会被触发。事件冒泡是默认的事件传播方式。

               / \
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
-----------------------------------

事件捕获(Event Capturing): 事件捕获是事件冒泡的另一种模式。在事件捕获中,事件会从根节点开始,依次向下沿着 DOM 树传播,直至达到事件的目标元素。然后,事件才会在目标元素上触发。

               | |
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------

事件委托(Event Delegation): 事件委托是一种利用事件冒泡原理的技术。它允许你将事件处理程序绑定到父元素而不是每个子元素上。通过在父元素上监听事件,你可以通过事件冒泡的方式捕获所有子元素上触发的事件,从而避免为每个子元素都绑定事件处理程序

其实从这几个概念来看就能看出来事件冒泡和事件捕获是反着的。

事件冒泡就是冲内往外,事件捕获就是从外往内

而事件委托就是一种方法,利用事件冒泡和事件捕获,把事件处理绑定到父元素,父元素通过监听捕获子元素然后触发事件

现在模型

现代浏览器中的事件传播模型是先事件捕获,然后是事件冒泡。这个模型被称为“DOM标准事件流”或“DOM3事件模型”。

  1. 事件捕获阶段(Event Capturing Phase):
    • 事件从文档根节点开始捕获,逐级向下直到达到触发事件的目标元素。
    • 在捕获阶段,可以通过在目标元素的祖先元素上注册事件处理函数来捕获事件。
  2. 目标阶段(Target Phase):
    • 事件达到目标元素后,触发在目标元素上注册的事件处理函数。
    • 这是事件的目标阶段,即事件的目标元素被操作。
  3. 事件冒泡阶段(Event Bubbling Phase):
    • 事件从目标元素开始向上冒泡,逐级冒泡到文档根节点。
    • 在冒泡阶段,可以通过在目标元素的祖先元素上注册事件处理函数来响应事件。

在使用addEventListener注册事件处理函数时,可以通过传递第三个参数来控制是在事件捕获阶段还是事件冒泡阶段进行处理。如果第三个参数为true,则在捕获阶段处理;如果为false或省略,则在冒泡阶段处理(默认)。

element.addEventListener('click', handler, true); // 在捕获阶段处理
element.addEventListener('click', handler); // 在冒泡阶段处理(默认)

总体而言,事件捕获和事件冒泡提供了一种机制,可以在DOM树的不同层次上响应和处理事件。

代码案例

  1. 事件冒泡(Event Bubbling):
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event Bubbling Example</title>
</head>
<body>
    <div id="parent">
        <button id="child">Click me!</button>
    </div>

    <script>
        document.getElementById('parent').addEventListener('click', function() {
            console.log('Parent bubbling');
        });

        document.getElementById('child').addEventListener('click', function() {
            console.log('Child clicked');
        });
    </script>
</body>
</html>

在这个例子中,点击按钮会触发子元素(按钮)上注册的事件处理函数,然后事件会冒泡到父元素,执行父元素上注册的事件处理函数。

  1. 事件捕获(Event Capturing):
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event Capturing Example</title>
</head>
<body>
    <div id="parent">
        <button id="child">Click me!</button>
    </div>

    <script>
        document.getElementById('parent').addEventListener('click', function() {
            console.log('Parent capturing');
        }, true); // 设置为true启用捕获阶段
    </script>
</body>
</html>

在这个例子中,点击按钮会触发父元素上注册的事件处理函数,然后事件会捕获到目标元素,执行目标元素上注册的事件处理函数。

  1. 事件委托(Event Delegation):
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event Delegation Example</title>
</head>
<body>
    <ul id="parentList">
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
    </ul>

    <script>
        document.getElementById('parentList').addEventListener('click', function(event) {
            if (event.target.tagName === 'LI') {
                console.log('Item clicked:', event.target.textContent);
            }
        });
    </script>
</body>
</html>

在这个例子中,点击列表中的任何一个<li>元素都会触发父元素(<ul>)上注册的事件处理函数。通过检查事件的target属性,可以确定点击的是哪个子元素。这样,无论添加或移除子元素,都无需更改事件处理函数。这就是事件委托的优势。

如何避免

  1. 取消事件默认行为和停止传播:

    • 在事件处理函数中,可以使用 event.preventDefault() 来取消事件的默认行为,以及 event.stopPropagation() 来停止事件的传播。
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Prevent Default and Stop Propagation Example</title>
    </head>
    <body>
        <div id="parent">
            <button id="child">Click me!</button>
        </div>
    
        <script>
            document.getElementById('child').addEventListener('click', function(event) {
                console.log('Child clicked');
                event.preventDefault(); // 阻止默认行为
                event.stopPropagation(); // 阻止事件传播
            });
        </script>
    </body>
    </html>
    

    在这个例子中,当按钮被点击时,不仅会阻止默认行为,还会阻止事件的传播,从而避免事件传播到父元素。

  2. 避免事件委托:

    • 不在父元素上注册代理处理子元素事件的事件处理函数。
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Avoid Event Delegation Example</title>
    </head>
    <body>
        <ul>
            <li id="item1">Item 1</li>
            <li id="item2">Item 2</li>
            <li id="item3">Item 3</li>
        </ul>
    
        <script>
            // 避免在父元素上注册事件委托
            document.getElementById('item1').addEventListener('click', function(event) {
                console.log('Item 1 clicked');
                event.stopPropagation(); // 阻止事件传播
            });
    
            document.getElementById('item2').addEventListener('click', function(event) {
                console.log('Item 2 clicked');
                event.stopPropagation(); // 阻止事件传播
            });
    
            document.getElementById('item3').addEventListener('click', function(event) {
                console.log('Item 3 clicked');
                event.stopPropagation(); // 阻止事件传播
            });
        </script>
    </body>
    </html>
    

    在这个例子中,直接在每个子元素上注册事件处理函数,同时阻止事件传播,以避免使用事件委托。