DOM 事件机制

1,914 阅读2分钟

事件冒泡和事件捕获是分别由IE和Netscape开发团队提出的几乎完全相反的事件流方案。事件流描述了页面接收事件的顺序。

事件冒泡

  • IE事件流被称为事件冒泡,这是因为事件被定义为从具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。

  • 比如有如下HTML页面:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Event Bubbling Example</title>
    </head>
    <body>
        <div id="myDiv">Click Me</div>
    </body>
    </html>
    

    在点击页面中的<div>元素后,click事件会以如下顺序发生:
    (1) <div>
    (2) <body>
    (3) <html>
    (4) document

事件捕获

  • Netscape团队提出了另一种名为事件捕获的事件流。事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。如果前面的例子使用事件捕获,则点击<div>元素会以下列顺序触发click事件:
    (1) document
    (2) <html>
    (3) <body>
    (4) <div>

DOM事件标准

  • 2002年,W3C发布标准,文档命名为DOM Level 2 Events Specification,规定浏览器应该同时支持两种调用顺序。
  • DOM事件标准描述了事件传播的三个阶段:
    1. 捕获阶段 — 事件(从Window)向下走近元素。
    2. 目标阶段 — 事件到达目标元素。
    3. 冒泡阶段 — 事件从元素上冒泡。

    如图下例子:点击 ,事件首先通过祖先链向下到达元素(捕获阶段),然后到达目标(目标阶段),最后上升(冒泡阶段),在途中调用处理程序(有处理程序就执行,没有就跳过)。

截屏2022-01-07 下午9.43.47.png

addEventListener绑定事件API

  • addEventListener有三个参数

    element.addEventListener(event, function, useCapture)
    
    • event:(必需)事件名,支持所有DOM事件。
    • function: (必需)指定要事件触发时执行的函数。
    • useCapture: (可选)指定事件是否在捕获或冒泡阶段执行。true,捕获;falsy值,冒泡;默认值为false。
  • 通过addEventListener添加的事件处理程序只能使用removeEventListener并传入与添加时同样的参数来移除。

    let btn = document.getElementById("myBtn")
    
    //不能移除
    btn.addEventListener("click", () => {
        console.log(this.id)
    }, false);
    
    
    //可以移除
    let handler = function() {
        console.log(this.id)
    }
    btn.addEventListener('click', handler, false);
    btn.removeEventListener('click', handler, false);
    

例子与小结

  • 栗子1

    <div id="box1">
        <div id="box2">
            <div id="box3">你好</div>
        </div>
    </div>
    <script>
        box1.onclick = function () {
            console.log("box1 51561");
        };
    
        box2.onclick = function () {
            console.log("box2");
        };
    
        box3.onclick = function () {
            console.log("box3");
        };
    
        box1.onclick = function () {
            console.log("box1");
        };
    
        box1.addEventListener("click",function () {
            console.log("box1 捕获阶段");
        },true);
    
        box2.addEventListener("click",function () {
            console.log("box2 捕获阶段");
        },true);
    
        box1.addEventListener("click",function () {
            console.log("box1 冒泡阶段");
        },false);
    
        box2.addEventListener("click",function () {
            console.log("box2 冒泡阶段");
        },false);
    
        box3.addEventListener("click",function () {
            console.log("box3 冒泡阶段");
        },false);
    
        box3.addEventListener("click",function () {
            console.log("box3 捕获阶段");
        },true);
    </script>
    
    //控制台输出什么?
    //box1 捕获阶段
    //box2 捕获阶段
    //box3 捕获阶段
    //box3
    //box3 冒泡阶段
    //box2
    //box2 冒泡阶段
    //box1
    //box1 冒泡阶段
    
  • 栗子2

    <div id="div1">
        <button>点击我</button>
    </div>
    <script>
        div1.addEventListener("click",() => {
            console.log("div clicked!");
        },true);
    
        let btn = document.querySelector("button");
        btn.addEventListener("click",() => {
            console.log("button clicked!");
        });
    
        document.addEventListener("click",() => {
            console.log("document clicked!");
        },true);
    </script>
    //控制台输出什么?
    //document clicked!
    //div clicked!
    //button clicked!
    //先捕获再冒泡
    
  • 栗子3

    <div class="grandfather">
        <div class="father">
            <div class="son">文字</div>
        </div>
    </div>
    
    // 给三个div分别添加事件监听fnYe/fnBa/fnEr
    
    • 提问1: 点击了谁? 点击文字,算不算点击儿子?点击文字,算不算点击爸爸?点击文字,算不算点击爷爷?(答案:都算。)
    • 提问2: 点击文字,最先调用fnYe/fnBa/fnEr中的哪一个函数?(答案:看开发者自己选择把fnYe/fnBa/fnEr放在捕获阶段还是放在冒泡阶段。)