JS事件冒泡与事件捕获

 
JavaScript 中,我们将事件发生的顺序称为“事件流”,当我们触发某个事件时,会发生一些列的连锁反应,例如有如下所示的一段代码:
<body>
    <div id="wrap">
        <p class="hint">
            <a href="#">Click Me</a>
        </p>
    </div>
</body>
如果给每个标签都定义事件,当我们点击其中的 <a> 标签时,会发现绑定在 <div> <p> 标签上的事件也被触发了,这到底是为什么呢?为了解答这一问题,微软和网景两公司提出了两种不同的概念,事件捕获与事件冒泡:
  • 事件捕获:由微软公司提出,事件从文档根节点(Document 对象)流向目标节点,途中会经过目标节点的各个父级节点,并在这些节点上触发捕获事件,直至到达事件的目标节点;
  • 事件冒泡:由网景公司提出,与事件捕获相反,事件会从目标节点流向文档根节点,途中会经过目标节点的各个父级节点,并在这些节点上触发捕获事件,直至到达文档的根节点。整个过程就像水中的气泡一样,从水底向上运动。

提示:上面提到的目标节点指的是触发事件的节点。

后来,W3C 为了统一标准,采用了一个折中的方式,即将事件捕获与事件冒泡合并,也就是现在的“先捕获后冒泡”,如下图所示:

事件捕获与事件冒泡
图:事件捕获与事件冒泡

事件捕获

在事件捕获阶段,事件会从 DOM 树的最外层开始,依次经过目标节点的各个父节点,并触发父节点上的事件,直至到达事件的目标节点。以上图中的代码为例,如果单击其中的 <a> 标签,则该事件将通过 document -> div -> p -> a的顺序传递到 <a> 标签。

示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</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("事件捕获: " + this.tagName);
        }

        var elems = document.querySelectorAll("div, p, a");
        for (let elem of elems) {
            elem.addEventListener("click", showTagName, true);
        }
    </script>
</body>
</html>
运行上面的代码,单击最内层的 <a> 标签,运行结果如下图所示:

事件捕获演示
图:事件捕获演示

事件冒泡

事件冒泡正好与事件捕获相反,事件冒泡是从目标节点开始,沿父节点依次向上,并触发父节点上的事件,直至文档根节点,就像水底的气泡一样,会一直向上。

示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</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('事件冒泡: ' + this.tagName)">DIV
        <p onclick="alert('事件冒泡: ' + this.tagName)">P
            <a href="#" onclick="alert('事件冒泡: ' + this.tagName)">A</a>
        </p>
    </div>
</body>
</html>
运行上面的代码,单击最内层的 <a> 标签,运行结果如下图所示:

事件冒泡演示
图:事件冒泡演示

阻止事件捕获和冒泡

了解了事件捕获和事件冒泡后会发现,这个特性并不友好,例如我们在某个节点上绑定了事件,本想在点击时触发这个事件,结果由于事件冒泡,这个节点的事件被它的子元素给触发了。我们要如何阻止这样的事情发生呢?

JavaScript 中提供了 stopPropagation() 方法来阻止事件捕获和事件冒泡的发生,语法格式如下:

event.stopPropagation();

注意:stopPropagation() 会阻止事件捕获和事件冒泡,但是无法阻止标签的默认行为,例如点击链接任然可以打开对应网页。

示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</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("您点击了 "+ this.tagName + " 标签");
            event.stopPropagation();
        }

        var elems = document.querySelectorAll("div, p, a");
        for(let elem of elems) {
            elem.addEventListener("click", showAlert);
        }
    </script>
</body>
</html>
此外,您也可以使用 stopImmediatePropagation() 方法来阻止同一节点的同一事件的其它事件处理程序,例如为某个节点定义了多个点击事件,当事件触发时,这些事件会按定义顺序依次执行,如果其中一个事件处理程序中使用了 stopImmediatePropagation() 方法,那么剩下的事件处理程序将不再执行。

stopImmediatePropagation() 方法的语法格式如下:

event.stopImmediatePropagation();

示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</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('您点击了 ' + this.tagName + ' 标签')">DIV
        <p onclick="alert('您点击了 ' + this.tagName + ' 标签')">P
            <a href="#" id="link">A</a>
        </p>
    </div>
    <script>
        function sayHi() {
            alert("事件处理程序 1");
            event.stopImmediatePropagation();
        }
        function sayHello() {
            alert("事件处理程序 2");
        }
        // 为 id 为 link 的标签定义多个点击事件
        var link = document.getElementById("link");
        link.addEventListener("click", sayHi); 
        link.addEventListener("click", sayHello);
    </script>
</body>
</html>

阻止默认操作

某些事件具有与之关联的默认操作,例如当您单击某个链接时,会自动跳转到指定的页面,当您单击提交按钮时,会将数据提交到服务器等。如果不想这样的默认操作发生,可以使用 preventDefault() 方法来阻止,其语法格式如下:

event.preventDefault();

示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript</title>
</head>
<body>
    <a href="http://c.biancheng.net/" id="link">链接</a>
    <script>
        var link = document.getElementById("link");
        link.addEventListener('click', function(event){
            event.preventDefault(); // 阻止链接跳转
        });
    </script>
</body>
</html>

注意:IE9 及以下的版本不支持 preventDefault() 方法,对于 IE9 及以下的浏览器您可以使用 event.returnValue = false;