65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript – 事件冒泡 – 图解

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2023年11月13日

CPOL

4分钟阅读

viewsIcon

5532

downloadIcon

69

关于 JavaScript 语言中“事件冒泡”的教程

1 引言

本文旨在提供一个关于浏览器 DOM 中“事件冒泡和捕获”的说明性例子。这并非一个完整的教程,而是一个关于事件如何在 DOM 中传播的“概念验证”。

2 理论背景

每个 DOM 节点都可以生成一个事件。事件是发生了某事的信号。

要对事件做出反应,我们需要为节点分配一个事件处理程序。有三种分配处理程序的方式:

  1. HTML 属性用法。例如:onclick=”myhandler(e)”;
  2. DOM 属性用法。例如:element.onclick=myhandler;
  3. JavaScript 方法。例如:element.addEventListener(e, myhandler, phase);

方法addEventListener的存在源于 DOM 中的每个Node都继承自EventTarget [2],后者充当根抽象类。因此,每个Node都可以接收事件并通过事件处理程序对其做出反应。

DOM 中的大多数事件都会传播。通常,事件传播有三个阶段:

  1. 捕获阶段:从 window 对象到特定目标事件的传播。
  2. 目标阶段:事件已到达其目标。
  3. 冒泡阶段:从目标到 window 对象的传播。

当事件发生时,会抛出一个事件对象。它包含描述该事件的属性。有两个属性对我们非常有用:

  • event.currentTarget – 处理该事件的对象(例如,实际单击的元素的某个父级,通过冒泡行为获取该事件)
  • event.target – 启动该事件的目标元素(例如,实际单击的元素)

3 示例 01 - 简单的事件传播演示

3.1 遍历所有节点

首先,我们要创建一个感兴趣的所有事件接收者的列表。这将包括我们 DOM 中的所有节点,加上documentwindow对象。这是相应的代码:

function getDescendants(node, myArray = null) {
    //note that we are looking for all Nodes, not just Elements
    //going recursively into dept
    var i;
    myArray = myArray || [];
    for (i = 0; i < node.childNodes.length; i++) {
        myArray.push(node.childNodes[i])
        getDescendants(node.childNodes[i], myArray);
    }
    return myArray;
}

function CreateListOfEventRecipients() {
    let result;
    //get all Nodes inside document
    result = getDescendants(document);
    //add Document object
    result.push(window.document);
    //add Window object
    result.push(window);
    return result;
}

3.2 日志事件

接下来,我们希望在事件处理函数中很好地记录事件。这是相应的代码:

function EventDescription(e, handlerPhase) {
    //function to log info from event object
    //here are properties, that are important
    //->handlerPhase -> we log info from which handler this event is coming 
    //                  from, is this from bubbling of capturing handler
    //->e.toString() -> class of event object
    //->e.type -> type of even, for example "click"
    //->e.timeStamp -> timestamp of event in milliseconds,
    //                 counted from load of the page
    //->e.target -> real target (object) of event, for example
    //              real object that was clicked on
    //->e.currentTarget -> current target (object) that is throwing 
    //                     this event, since event got here by either
    //                     bubbling of capturing propagation
    //->e.eventPhase -> real phase of event, regardless of handler that
    //                  created event handler, can be 1-capturing, 
    //                  2-target, 3-bubbling phase
    /* Sample execution:
    6600.900000095367[object PointerEvent]-----------
    ......EventType:click..HandlerPhase:Capturing..EventPhase:1 (Capturing)
    ......Target:[object HTMLElement], nodeName:B, id:Bold4
    ......CurrentTarget:[object HTMLDivElement], nodeName:DIV, id:Div1
    */
    const dots = "......";
    let result;
    if (e !== undefined && e !== null) {
        let eventObject = e.toString();
        let eventType = (e.type) ?
            (e.type.toString()) : undefined;
        let eventTimestamp = (e.timeStamp) ?
            (e.timeStamp.toString()) : undefined;
        let eventTarget = (e.target) ? ObjectDescription(e.target) : undefined;
        let eventCurrentTarget = (e.currentTarget) ?
            ObjectDescription(e.currentTarget) : undefined;
        let eventPhase = (e.eventPhase) ?
            PhaseDescription(e.eventPhase) : undefined;

        result = "";
        result += (eventTimestamp) ? eventTimestamp : "";
        result += (eventObject) ? eventObject : "";
        result += "-----------<br>";
        result += dots;
        result += (eventType) ? ("EventType:" + eventType) : "";
        result += (handlerPhase) ? ("..HandlerPhase:" + handlerPhase) : "";
        result += (eventPhase) ? ("..EventPhase:" + eventPhase) : "";
        result += "<br>";
        result += (eventTarget) ? (dots + "Target:" + eventTarget + "<br>") : "";
        result += (eventCurrentTarget) ? (dots + "CurrentTarget:" +
            eventCurrentTarget + "<br>") : "";
    }
    return result;
}  

3.3 完整示例 01 代码

这是完整的Example01代码,因为大多数人都喜欢可以复制粘贴的代码。

<!DOCTYPE html>
<html>

<body>
    <!--Html part---------------------------------------------->
    <div id="Div1" style="border: 1px solid; padding:10px; 
        margin:20px; background-color:aqua;">
        Div1
        <div id="Div2" style="border: 1px solid; padding:10px; 
            margin:20px;background-color:chartreuse">
            Div2
            <div id="Div3" style="border: 1px solid; padding:10px;
                margin:20px; background-color:yellow; ">
                Div3<br/>
                Please click on 
                <b id="Bold4" style="font-size:x-large;">bold text</b> only.
            </div>
        </div>
    </div>
    <hr />
    <h3>Example 01</h3>
    <br />
    <button onclick="task1()"> Task1-List of all nodes</button><br />
    <br />
    <button onclick="task2()"> Task2-Activate EventHandlers</button><br />
    <br />
    <button onclick="task3()"> Task3-Clear Output Box (delayed 1 sec)</button><br />
    <hr />

    <h3>Output</h3>
    <div id="OutputBox" style="border: 1px solid; min-height:20px">
    </div>

    <!--JavaScript part---------------------------------------------->
    <script>

        function OutputBoxClear() {
            document.getElementById("OutputBox").innerHTML = "";
        }

        function OutputBoxWriteLine(textLine) {
            document.getElementById("OutputBox").innerHTML
                += textLine + "<br/>";
        }

        function OutputBoxWrite(textLine) {
            document.getElementById("OutputBox").innerHTML
                += textLine;
        }

        function ObjectDescription(oo) {
            //expecting Node or Window or Document
            //logging some basic info to be able to 
            //identify which object is that in the DOM tree
            let result;

            if (oo != null) {
                result = "";
                result += oo.toString();

                if (oo.nodeName !== undefined) {
                    result += ", nodeName:" + oo.nodeName;
                }

                if (oo.id !== undefined && oo.id !== null
                    && oo.id.trim().length !== 0) {
                    result += ", id:" + oo.id;
                }

                if (oo.data !== undefined) {
                    let myData = oo.data;
                    let length = myData.length;
                    if (length > 30) {
                        myData = myData.substring(0, 30);
                    }
                    result += `, data(length ${length}):` + myData;
                }
            }

            return result;
        }

        function PhaseDescription(phase) {
            //some text to explain phase numbers
            let result;
            if (phase !== undefined && phase !== null) {
                switch (phase) {
                    case 1:
                        result = "1 (Capturing)";
                        break;
                    case 2:
                        result = "2 (Target)";
                        break;
                    case 3:
                        result = "3 (Bubbling)";
                        break;
                    default:
                        result = phase;
                        break;
                }
            }
            return result;
        }

        function EventDescription(e, handlerPhase) {
            //function to log info from event object
            //here are properties, that are important
            //->handlerPhase -> we log info from which handler this event is coming 
            //                  from, is this from bubbling of capturing handler
            //->e.toString() -> class of event object
            //->e.type -> type of even, for example "click"
            //->e.timeStamp -> timestamp of event in milliseconds,
            //                 counted from load of the page
            //->e.target -> real target (object) of event, for example
            //              real object that was clicked on
            //->e.currentTarget -> current target (object) that is throwing 
            //                     this event, since event got here by either
            //                     bubbling of capturing propagation
            //->e.eventPhase -> real phase of event, regardless of handler that
            //                  created event handler, can be 1-capturing, 
            //                  2-target, 3-bubbling phase
            /* Sample execution:
            6600.900000095367[object PointerEvent]-----------
            ......EventType:click..HandlerPhase:Capturing..EventPhase:1 (Capturing)
            ......Target:[object HTMLElement], nodeName:B, id:Bold4
            ......CurrentTarget:[object HTMLDivElement], nodeName:DIV, id:Div1
            */
            const dots = "......";
            let result;
            if (e !== undefined && e !== null) {
                let eventObject = e.toString();
                let eventType = (e.type) ?
                    (e.type.toString()) : undefined;
                let eventTimestamp = (e.timeStamp) ?
                    (e.timeStamp.toString()) : undefined;
                let eventTarget = (e.target) ? ObjectDescription(e.target) : undefined;
                let eventCurrentTarget = (e.currentTarget) ?
                    ObjectDescription(e.currentTarget) : undefined;
                let eventPhase = (e.eventPhase) ?
                    PhaseDescription(e.eventPhase) : undefined;

                result = "";
                result += (eventTimestamp) ? eventTimestamp : "";
                result += (eventObject) ? eventObject : "";
                result += "-----------<br>";
                result += dots;
                result += (eventType) ? ("EventType:" + eventType) : "";
                result += (handlerPhase) ? ("..HandlerPhase:" + handlerPhase) : "";
                result += (eventPhase) ? ("..EventPhase:" + eventPhase) : "";
                result += "<br>";
                result += (eventTarget) ? (dots + "Target:" + eventTarget + "<br>") : "";
                result += (eventCurrentTarget) ? (dots + "CurrentTarget:" +
                    eventCurrentTarget + "<br>") : "";
            }
            return result;
        }

        function getDescendants(node, myArray = null) {
            //note that we are looking for all Nodes, not just Elements
            //going recursively into dept
            var i;
            myArray = myArray || [];
            for (i = 0; i < node.childNodes.length; i++) {
                myArray.push(node.childNodes[i])
                getDescendants(node.childNodes[i], myArray);
            }
            return myArray;
        }

        function CreateListOfEventRecipients() {
            let result;
            //get all Nodes inside document
            result = getDescendants(document);
            //add Document object
            result.push(window.document);
            //add Window object
            result.push(window);
            return result;
        }

        function MyEventHandler(event, handlerPhase) {
            OutputBoxWrite(EventDescription(event, handlerPhase));
        }

        function BubblingEventHandler(event) {
            MyEventHandler(event, "Bubbling");
        }

        function CapturingEventHandler(event) {
            MyEventHandler(event, "Capturing");
        }

        window.onerror = function (message, url, line, col, error) {
            OutputBoxWriteLine(`Error:${message}\n At ${line}:${col} of ${url}`);
        };

        function task1() {
            //we need to delay for 1 second aaa 
            //to avoid capturing click on button itself 
            setTimeout(task1_worker, 1000);
        }
        function task1_worker() {
            //show all Nodes plus Window plus Document 
            //they all receive events
            OutputBoxClear();
            OutputBoxWriteLine("Task1");
            let arrayOfEventRecipientCandidates = CreateListOfEventRecipients();

            for (let i = 0; i < arrayOfEventRecipientCandidates.length; ++i) {
                let description = ObjectDescription(arrayOfEventRecipientCandidates[i]);
                OutputBoxWriteLine(`[${i}] ${description}`);
            }
        }

        function task2() {
            OutputBoxClear();
            OutputBoxWriteLine("Task2");
            //we need to delay for 1 second creation of EventsHandlers
            //to avoid capturing click on button Task2 itself 
            setTimeout(task2_worker, 1000);
        }

        function task2_worker() {
            //create list of all Event Recipients
            //and assigning Events Handlers
            let arrayOfEventRecipientCandidates = CreateListOfEventRecipients();

            for (let i = 0; i < arrayOfEventRecipientCandidates.length; ++i) {
                //we check that each member of list can receive events
                if ("addEventListener" in arrayOfEventRecipientCandidates[i]) {
                    //adding Event Handlers for Bubbling phase
                    //actually, this will be handler for bubbling and target phase
                    arrayOfEventRecipientCandidates[i].addEventListener
                                ("click", BubblingEventHandler);
                    //adding Event Handlers for Capturing phase
                    //actually, this will be handler for capturing and target phase
                    arrayOfEventRecipientCandidates[i].addEventListener
                                ("click", CapturingEventHandler, true);
                }
                else {
                    //here printout if any object from the list can not receive Events
                    let description = "Object does not have addEventListener:" +
                        ObjectDescription(arrayOfEventRecipientCandidates[i]);
                    OutputBoxWriteLine(`[${i}] ${description}`);
                }
            }
        }

        function task3() {
            //we need to delay for 1 second aaa 
            //to avoid capturing click on button Task3 itself 
            setTimeout(OutputBoxClear, 1000);
        }

    </script>
</body>
<!--
Output

    -->

</html>

3.4 应用程序截图

这是应用程序的外观:

3.5 执行 – 查找所有事件目标

首先,我们将展示我们的方法找到的所有事件目标。这包括所有节点以及documentwindow对象。该列表上大约有 60 个对象。请注意,它包括所有节点,包括文本和注释节点。

3.6 执行 – 激活事件处理程序

然后,我们激活列表中所有对象的事件处理程序。

3.7 执行 – 单击事件

现在我们执行单击操作。这是应用程序中的事件日志:

3.8 评论

对于喜欢 DOM 树图的人,这是应用程序中发生的事情的图表。

  • 请注意,上面的图表不包括documentwindow对象,正如从日志中可以看出的,它们也是单击事件的接收者。
  • 请注意,虽然我们实际上单击了文本节点#text-6,但该节点没有收到事件,但包含它的元素 B Id:Bold4收到了单击事件。

4 示例 02

我创建此示例的原因是因为我在互联网上看到有声明说,在目标阶段,事件并不总是以Capturing-Bubbling顺序运行,而是以它们在代码中定义的顺序运行。我发现这些说法不真实,至少对于我用于测试的这个 Chrome 版本而言。

4.2 代码

我不会把整个代码放在这里,因为它与上一个例子类似。这里只是关键部分:

function task1() {
    OutputBoxClear();
    OutputBoxWriteLine("Task1-Activate EventHandlers");

    //Div1 event handlers
    let div1=document.getElementById("Div1");
    div1.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling"));
    div1.addEventListener("click", (e)=>MyEventHandler(e, "Capturing"), true);

    //Div2 event handlers
    let div2=document.getElementById("Div2");
    div2.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling"));
    div2.addEventListener("click", (e)=>MyEventHandler(e, "Capturing"), true);

    //Div3 event handlers
    //we deliberately mix defining order of bubbling and capturing events
    //to see order when events are fired (**)
    let div3=document.getElementById("Div3");
    //actually, this will be handler for bubbling and target phase
    div3.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling"));
    //actually, this will be handler for capturing and target phase
    div3.addEventListener("click", (e)=>MyEventHandler(e, "Capturing"), true);
    //actually, this will be handler for bubbling and target phase
    div3.addEventListener("click", (e)=>MyEventHandler(e, "Bubbling2"));
    //actually, this will be handler for capturing and target phase
    div3.addEventListener("click", (e)=>MyEventHandler(e, "Capturing2"), true);
}       

请注意,在 (**) 中,我们交错定义了同一元素的CapturingBubbling事件处理程序。

4.3 截图

这是应用程序截图。

4.4 执行 – 我们的点击

现在我们进行点击。这是生成的日志:

就我所见(在这个 Chrome 版本上),在目标阶段,所有事件都按照事件处理程序捕获优先,然后是Bubbling的顺序运行。处理程序不会按照 (**) 中定义的顺序运行。
如果你查看目标阶段(黄色),你将看到事件的顺序是 “Capturing”, “Capturing2”, “Bubbling”, “Bubbling2”。互联网上一些人声称顺序将与定义 (**) 中的顺序相同,例如 “Capturing”, “Bubbling”, “Capturing2”, “Bubbling2”。但是,我的这个实验证明了这种说法是不成立的,至少对于这个 Chrome 版本来说。

5 结论

在本文中,我们提供了一个简单的“概念验证”应用程序,展示了 DOM 中的事件传播是如何工作的。

6 参考资料

7 历史

  • 2023 年 11 月 13 日:初始版本
© . All rights reserved.