JavaScript 事件传播

在本教程中,你将了解事件如何在 JavaScript 中的 DOM 树中传播。

了解事件传播

事件传播是一种机制,它定义事件如何通过 DOM 树传播或传播到达目标,以及之后发生的事情。

让我们在一个例子的帮助下理解这一点,假设你已经在一个嵌套在一个段落(即<p>元素)中的超链接(即 <a> 元素)上分配了一个 click 事件处理程序。现在,如果单击该链接,将执行处理程序。但是,如果将 click 事件处理程序分配给包含该链接的段落,而不是链接,那么即使在这种情况下,单击该链接仍将触发处理程序。这是因为事件不仅影响生成事件的目标元素 - 它们在 DOM 树中上下移动以到达目标。这称为事件传播。

在现代浏览器中,事件传播分两个阶段进行: 捕获冒泡阶段。在我们继续前进之前,请看一下下图:

事件传播演示

上图显示了在具有父元素的元素上触发事件时,事件在事件传播的不同阶段中事件如何在 DOM 树中传播。

引入事件传播的概念是为了处理 DOM 层次结构中具有父子关系的多个元素具有针对同一事件的事件处理程序的情况,例如鼠标单击。现在,问题是当用户点击内部元素 - 外部元素的 click 事件或内部元素时,将首先处理哪个元素的 click 事件。

在本章的以下部分中,我们将更详细地讨论事件传播的每个阶段,并找出该问题的答案。

注意: 正式的有 3 个阶段,捕获目标泡沫阶段。但是,第二阶段即目标阶段(在事件到达已生成事件的目标元素时发生)在现代浏览器中不单独处理,因此我们用捕获冒泡两个阶段来处理程序。

捕获阶段

在捕获阶段,事件从 Window 向下传播通过 DOM 树到目标节点。例如,如果用户单击超链接,则该单击事件将通过元素 <html><body> 元素和包含该链接的<p>元素。

此外,如果目标元素的任何祖先(即父,祖父等)和目标本身具有针对该类型事件的特别注册的捕获事件监听器,则在该阶段期间执行这些监听器。我们来看看下面的例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Capturing Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    function showTagName() {
        alert("Capturing: "+ this.tagName);
    }
    
    var elems = document.querySelectorAll("div, p, a");
    for(let elem of elems) {
        elem.addEventListener("click", showTagName, true);
    }
</script>
</body>
</html>

以下是我们利用上面的示例创建的简单演示,向你展示事件捕获的工作原理。单击任何元素并观察出现警报弹出窗口的顺序。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

并非所有浏览器都支持事件捕获,而且它很少使用。例如,版本 9.0 之前的 Internet Explorer 不支持事件捕获。

此外,事件捕获仅适用于 addEventListener() 在第三个参数设置为 true 时使用该方法注册的事件处理程序。分配事件处理程序,如使用的传统方法 onclickonmouseover 等会不会在这里工作。请查看 JavaScript 事件监听器 章节以了解有关事件监听器的更多信息。

冒泡阶段

在冒泡阶段,恰好相反。在这个阶段,事件传播或冒泡 DOM 树,从目标元素到窗口,逐个访问目标元素的所有祖先。例如,如果用户单击超链接,则该单击事件将通过 <p> 包含链接, <body> 元素, <html> 元素和 document 节点的元素。

此外,如果目标元素的任何祖先和目标本身具有为该类型的事件分配的事件处理程序,则在此阶段执行这些处理程序。在现代浏览器中,默认情况下,所有事件处理程序都在冒泡阶段进行注册。我们来看一个例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Bubbling Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div onclick="alert('Bubbling: ' + this.tagName)">DIV
    <p onclick="alert('Bubbling: ' + this.tagName)">P
        <a href="#" onclick="alert('Bubbling: ' + this.tagName)">A</a>
    </p>
</div>
</body>
</html>

这是一个简单的演示,我们利用上面的例子创建了一个简单的演示,向你展示事件冒泡是如何工作的。单击任何元素并观察出现警报弹出窗口的顺序。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

所有浏览器都支持事件冒泡,它适用于所有处理程序,无论它们如何注册,例如使用 onclickaddEventListener() (除非它们被注册为捕获事件监听器)。这就是为什么术语事件传播经常被用作事件冒泡的同义词。

访问目标元素

目标元素是生成事件的 DOM 节点 。例如,如果用户单击超链接,则目标元素是超链接。

目标元素是可访问的 event.target ,它不会通过事件传播阶段发生变化。此外, this 关键字表示当前元素(即附加有当前正在运行的处理程序的元素)。我们来看一个例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Target Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;			
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    // Selecting the div element
    var div = document.getElementById("wrap");

    // Attaching an onclick event handler
    div.onclick = function(event) {
        event.target.style.backgroundColor = "lightblue";

        // Let the browser finish rendering of background color before showing alert
        setTimeout(() => {
            alert("target = " + event.target.tagName + ", this = " + this.tagName);
            event.target.style.backgroundColor = ''
        }, 0);
    }
</script>
</body> 
</html>

这是我们利用上面的例子创建的简单演示。单击任何元素,它将显示目标元素的标记名称和当前元素。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

我们在上面的例子中使用的胖箭头(=>)符号是箭头函数表达式。它的语法比函数表达式短,并且可以使 this 关键字正常运行。请查看有关 ES6 功能 的教程,以了解有关箭头功能的更多信息。

停止事件传播

如果要阻止任何祖先元素的事件处理程序收到有关该事件的通知,你还可以在中间停止事件传播。

例如,假设你有嵌套元素,并且每个元素都有 onclick 显示警告对话框的事件处理程序。通常,当你单击内部元素时,将立即执行所有处理程序,因为事件会冒泡到 DOM 树。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Propagation Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    function showAlert() {
        alert("You clicked: "+ this.tagName);
    }

    var elems = document.querySelectorAll("div, p, a");
    for(let elem of elems) {
        elem.addEventListener("click", showAlert);
    }
</script>
</body>
</html>

这是我们利用上面的例子创建的简单演示。如果单击任何子元素,则还会执行父元素上的事件处理程序,你可能会看到多个警告框。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

为了防止出现这种情况,你可以使用 event.stopPropagation() 方法阻止事件冒泡 DOM 树。在以下示例中,如果单击子元素,则不会执行父元素上的单击事件侦听器。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Event Propagation Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div id="wrap">DIV
    <p class="hint">P
        <a href="#">A</a>
    </p>
</div>

<script>
    function showAlert(event) {
        alert("You clicked: "+ this.tagName);
        event.stopPropagation();
    }

    var elems = document.querySelectorAll("div, p, a");
    for(let elem of elems) {
        elem.addEventListener("click", showAlert);
    }
</script>
</body>
</html>

这是更新的演示。现在,如果单击任何子元素,则只会出现一个警报。

<div id="wrap">
<p class="hint">
<a href="#">Click Me</a>
</p>

</div>

此外,你甚至可以使用 stopImmediatePropagation() 方法来阻值连接到同一元素的任何其他侦听器以获取相同的事件类型执行。

在下面的示例中,我们将多个侦听器附加到超链接,但是当你单击该链接时,只会执行一个超链接侦听器,并且你将只看到一个警报。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Immediate Propagation Demo</title>
<style type="text/css">
    div, p, a{
        padding: 15px 30px;
        display: block;
        border: 2px solid #000;
        background: #fff;
    }
</style>
</head>
<body>
<div onclick="alert('You clicked: ' + this.tagName)">DIV
    <p onclick="alert('You clicked: ' + this.tagName)">P
        <a href="#" id="link">A</a>
    </p>
</div>

<script>
    function sayHi() {
        alert("Hi, there!");
        event.stopImmediatePropagation();
    }
    function sayHello() {
        alert("Hello World!");
    }
    
    // Attaching multiple event handlers to hyperlink
    var link = document.getElementById("link");
    link.addEventListener("click", sayHi);  
    link.addEventListener("click", sayHello);
</script>
</body>
</html>

注意: 如果多个侦听器附加到同一事件类型的同一元素,则它们将按照添加它们的顺序执行。但是,如果任何侦听器调用该 event.stopImmediatePropagation() 方法,则不会执行任何剩余的侦听器。

防止默认操作

某些事件具有与之关联的默认操作。例如,如果单击链接浏览器,则会转到链接的目标,当你单击表单提交按钮时,浏览器提交表单等。你可以使用事件对象的 preventDefault() 方法阻止此类默认操作。

但是,阻止默认操作不会停止事件传播; 事件继续像往常一样传播到 DOM 树。这是一个例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Prevent Default Demo</title>
</head>
<body>
<form action="/examples/html/action.php" method="post" id="users">
    <label>First Name:</label>
    <input type="text" name="first-name" id="firstName">
    <input type="submit" value="Submit" id="submitBtn">
</form>

<script>
    var btn = document.getElementById("submitBtn");
    
    btn.addEventListener("click", function(event) {
        var name = document.getElementById("firstName").value;
        alert("Sorry, " + name + ". The preventDefault() won't let you submit this form!");
        event.preventDefault(); // Prevent form submission
    });
</script>
</body>
</html>