JAVASCRIPT 基础教程
JAVASCRIPT & DOM
JAVASCRIPT & BOM
JAVASCRIPT 高级教程
JAVASCRIPT 示例
JAVASCRIPT 参考

JavaScript 事件传播

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

了解事件传播

事件传播是一种机制,它定义了事件如何传播或穿过 DOM 树以到达其目标以及之后会发生什么。

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

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

Event Propagation Demo

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

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

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

注: 正式有 3 个阶段,捕获目标冒泡 阶段。 但是,第二阶段,即目标阶段(当事件到达已生成事件的目标元素时发生)在现代浏览器中没有单独处理,处理程序注册为 capturingbubbling 阶段在此阶段执行。

捕获阶段

在捕获阶段,事件从 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 不支持事件捕获。

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


冒泡阶段

在冒泡阶段,情况正好相反。 在这个阶段,事件从目标元素到 Window 向上传播或冒泡备份 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>
    // 选择 div 元素
    var div = document.getElementById("wrap");

    // 附加 onclick 事件处理程序
    div.onclick = function(event) {
        event.target.style.backgroundColor = "lightblue";

        // 让浏览器在显示警报之前完成背景颜色的渲染
        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!");
    }
    
    // 将多个事件处理程序附加到超链接
    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.asp" 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(); // 阻止表单提交
    });
</script>
</body>
</html>
Advertisements