DOM事件机制详解

事件冒泡(event bubbling)和事件捕获(event capturing)是事件传播的两种方法。在早期,网景(Netscape)推崇事件捕获模式,而微软IE浏览器支持事件冒泡模式。如今,IE9+及主流浏览器两种模式都支持。

事件冒泡

事件冒泡模式流程:事件发生时,先触发目标元素(最直接元素)的事件响应函数,然后触发其父元素的事件响应函数,并逐级上溯到祖先元素。

假设有如下的html结构,3个元素嵌套在一起DIV > P > BUTTON

 <div style="border:solid 1px #000">
    DIV
    <p style="border:solid 1px #000">
        P<button>BUTTON</button>
    </p>
 </div>
DIV

P

如果点击最内层按钮,按照事件冒泡流程,事件响应顺序是:
  1. 按钮响应事件
  2. P元素响应事件
  3. DIV元素响应事件
  4. 事件继续向上传播,BODY,DOCUMENT等响应事件,在有些浏览器上,事件一直向上传播到WINDOWS对象

事件捕获

事件捕获模式流程正好与冒泡模式相反,事件发生时,先触发最外层元素(祖先元素)的事件响应函数,逐级向下,直到目标元素。

还是上面的例子,如果点击按钮,在事件捕获流程下,事件响应的顺序是:

  1. DIV元素响应事件
  2. P元素响应事件
  3. 按钮响应事件

DOM事件流

DOM事件流分为三个阶段

  1. 事件捕获阶段 - 事件流由上往下直到目标元素
  2. 目标阶段 - 事件到达目标元素
  3. 事件冒泡阶段 - 事件流由下往上从目标元素直到最外层祖先元素

通常我们都只使用冒泡阶段的事件响应,可以用html元素的on(event)属性指定,也可以使用addEventListener(event, handler)指定,其实addEventListener还有第三个参数,如果需要指定捕获阶段的事件响应,设置第三个参数为true。 下面通过一个简单的例子展示了DOM的事件流:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DOM事件流</title>
</head>
<body>
    <div id="div1" style="border:1px solid #000;padding: 30px">
        DIV1
        <div id="div2"  style="border:1px solid #000;padding: 30px">
            DIV2
            <div id="div3"  style="border:1px solid #000; padding: 30px">DIV3</div>
        </div>
    </div>
</body>
<script type="text/javascript">
    var div1 = document.getElementById('div1');
    var div2 = document.getElementById('div2');
    var div3 = document.getElementById('div3');
    // true表示在捕获阶段处理事件、false表示在冒泡阶段处理
    div1.addEventListener('click',function (event) {
        console.log('事件捕获阶段: div1点击事件');
    }, true);
    div1.addEventListener('click',function () {
        console.log('事件冒泡阶段: div1点击事件');
    }, false);
    div2.addEventListener('click',function () {
        console.log('事件捕获阶段: div2点击事件');
    }, true);
    div2.addEventListener('click',function () {
        console.log('事件冒泡阶段: div2点击事件');
    }, false)
    div3.addEventListener('click',function () {
        console.log('事件捕获阶段: div3点击事件');
    }, true);
    div3.addEventListener('click',function () {
        console.log('事件冒泡阶段: div3点击事件');
    }, false)
</script>
</html>

在Chrome里执行,

DOM事件流
DIV1
DIV2
DIV3
点击*DIV3*控制台输出如下:
事件捕获阶段: div1点击事件
事件捕获阶段: div2点击事件
事件捕获阶段: div3点击事件
事件冒泡阶段: div3点击事件
事件冒泡阶段: div2点击事件
事件冒泡阶段: div1点击事件

事件目标元素

无论事件传播采用什么流程传播,事件响应函数中总是包含目标元素的信息,由event.target指定,注意event.targetthis的区别:

  • event.target:目标元素,最直接引起事件的元素
  • this/event.currentTarget:当前事件响应函数绑定的元素

DOM事件流阻断

DOM事件流可以在经过的任意事件处理函数中终止。无论当前执行的事件响应函数是处于捕获阶段还是冒泡阶段,方法event.stopPropagation()终止事件流的继续传播。

如果一个元素对同一事件绑定了多个事件响应函数,即使在其中一个响应函数中调用了event.stopPropagation(),该元素上的其它事件响应函数仍然会被执行。也可以这样理解,event.stopPropagation()只是阻止事件传递到下一个元素,并不阻止当前元素上事件响应。如果要同时阻止当前元素上其它事件响应函数的执行,需要调用方法event.stopImmediatePropagation(),它确保事件响应到此为止,后续所有事件响应函数都不会执行。

小结

目前主流的浏览器事件传播流程都遵循DOM的事件流机制,即先走由外到内捕获流程,然后是由内到外的冒泡流程。大多数情况下,都只在冒泡阶段响应事件,捕获事件很少使用。虽然stopPropagation可以阻断事件流,但是通常不推荐使用,只有在确实有必要时使用。