目录
1 引言
本文的目的是提供浏览器DOM中“事件的冒泡和捕获”的说明性示例。这并不是一个完整的教程,而是事件如何在DOM中传播的“概念证明”。
2 理论背景
每个DOM节点都可以生成一个事件。事件是某事发生的信号。
为了对事件做出反应,我们需要为节点分配一个事件处理程序。有三种方法可以分配处理程序:
- Html属性用法。例如:οnclick=”myhandler(e)”;
- DOM属性用法。例如:element.οnclick=myhandler;
- JavaScript方法。例如:element.addEventListener(e, myhandler, phase);
该addEventListener方法的存在源于这样一个事实,即DOM中的每个Node都继承自EventTarget[2],它充当根抽象类。因此,每个Node都可以通过事件处理程序接收事件并对它们做出反应。
DOM中的大多数事件都会传播。通常,事件传播分为三个阶段:
- 捕获阶段:从窗口对象传播到特定目标事件。
- 目标阶段:事件已达到其目标。
- 冒泡阶段:从目标向窗口对象传播。
事件发生时将抛出事件对象。它包含描述该事件的属性。我们非常感兴趣的两个属性:
- event.currentTarget——处理事件的对象(例如,实际单击的元素的某个父元素,通过冒泡行为获取该事件)
- event.target——启动事件的目标元素(例如,实际单击该事件的元素)
3 Example01——简单事件传播演示
3.1 遍历所有节点
首先,我们要创建一个所有感兴趣的事件收件人的列表。这将包括我们的 DOM plus 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对象,从日志中可以看出,这些对象也是click事件的接收者。
- 请注意,虽然我们实际上单击了文本Node #text-6,但该Node没有收到该事件,但包含它的Element B Id:Bold4 确实收到了click事件。
4 Example02
我创建此示例的原因是,我在Internet上看到这样一种说法:在Target阶段,事件并不总是按Capturing-Bubbling顺序运行,而是按在代码中定义的顺序运行。我发现这些说法是不真实的,至少对于我用于测试的这个版本的Chrome是这样。
4.1 代码
我不会把整个代码放在这里,因为它与前面的例子类似。以下是关键部分:
unction 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.2 截图
这是应用程序屏幕截图。
4.3 执行——我们的点击
现在我们点击一下。下面是生成的日志:
据我所知(在此版本的Chrome上),在Target阶段,所有事件都首先按照Event-Handlers Capturing的顺序运行,然后是Bubbling。处理程序不按(**)中定义的顺序运行。
如果您查看目标阶段(黄色),您将看到此执行中事件的顺序“Capturing”、“Capturing 2”、“Bubbling”、“Bubbling 2”。互联网上一些人声称,顺序将与定义(**)中的顺序相同,例如“Capturing”,“Bubbling”,“Bubbling 2”,“Bubbling2”。但是,我的这个实验反驳了这种说法,至少对于这个版本的Chrome来说是这样。
5 结论
在本文中,我们提供了一个简单的“概念验证”应用程序,展示了DOM中的事件传播是如何工作的。
6 参考资料
https://www.codeproject.com/Articles/5372580/JavaScript-Events-Bubbling-Illustrated