JavaScript – 事件冒泡 – 图解
关于 JavaScript 语言中“事件冒泡”的教程
1 引言
本文旨在提供一个关于浏览器 DOM 中“事件冒泡和捕获”的说明性例子。这并非一个完整的教程,而是一个关于事件如何在 DOM 中传播的“概念验证”。
2 理论背景
每个 DOM 节点都可以生成一个事件。事件是发生了某事的信号。
要对事件做出反应,我们需要为节点分配一个事件处理程序。有三种分配处理程序的方式:
- HTML 属性用法。例如:
onclick=”myhandler(e)”;
- DOM 属性用法。例如:
element.onclick=myhandler;
- JavaScript 方法。例如:
element.addEventListener(e, myhandler, phase);
方法addEventListener
的存在源于 DOM 中的每个Node
都继承自EventTarget
[2],后者充当根抽象类。因此,每个Node
都可以接收事件并通过事件处理程序对其做出反应。
DOM 中的大多数事件都会传播。通常,事件传播有三个阶段:
- 捕获阶段:从 window 对象到特定目标事件的传播。
- 目标阶段:事件已到达其目标。
- 冒泡阶段:从目标到 window 对象的传播。
当事件发生时,会抛出一个事件对象。它包含描述该事件的属性。有两个属性对我们非常有用:
event.currentTarget
– 处理该事件的对象(例如,实际单击的元素的某个父级,通过冒泡行为获取该事件)event.target
– 启动该事件的目标元素(例如,实际单击的元素)
3 示例 01 - 简单的事件传播演示
3.1 遍历所有节点
首先,我们要创建一个感兴趣的所有事件接收者的列表。这将包括我们 DOM 中的所有节点,加上document
和window
对象。这是相应的代码:
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 执行 – 查找所有事件目标
首先,我们将展示我们的方法找到的所有事件目标。这包括所有节点以及document
和window
对象。该列表上大约有 60 个对象。请注意,它包括所有节点,包括文本和注释节点。
3.6 执行 – 激活事件处理程序
然后,我们激活列表中所有对象的事件处理程序。
3.7 执行 – 单击事件
现在我们执行单击操作。这是应用程序中的事件日志:
3.8 评论
对于喜欢 DOM 树图的人,这是应用程序中发生的事情的图表。
- 请注意,上面的图表不包括
document
和window
对象,正如从日志中可以看出的,它们也是单击事件的接收者。 - 请注意,虽然我们实际上单击了文本节点#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);
}
请注意,在 (**) 中,我们交错定义了同一元素的Capturing
和Bubbling
事件处理程序。
4.3 截图
这是应用程序截图。
4.4 执行 – 我们的点击
现在我们进行点击。这是生成的日志:
就我所见(在这个 Chrome 版本上),在目标阶段,所有事件都按照事件处理程序捕获优先,然后是Bubbling
的顺序运行。处理程序不会按照 (**) 中定义的顺序运行。
如果你查看目标阶段(黄色),你将看到事件的顺序是 “Capturing”, “Capturing2”, “Bubbling”, “Bubbling2”。互联网上一些人声称顺序将与定义 (**) 中的顺序相同,例如 “Capturing”, “Bubbling”, “Capturing2”, “Bubbling2”。但是,我的这个实验证明了这种说法是不成立的,至少对于这个 Chrome 版本来说。
5 结论
在本文中,我们提供了一个简单的“概念验证”应用程序,展示了 DOM 中的事件传播是如何工作的。
6 参考资料
7 历史
- 2023 年 11 月 13 日:初始版本