谁能解释一下 JavaScript 中的事件委托,它有什么用?
什么是 DOM 事件委托?
DOM 事件委托是一种通过事件“冒泡”(又名事件传播)的魔法,通过单个公共父级而不是每个子级响应 ui 事件的机制。
当在元素上触发事件时,会发生以下情况:
事件被分派到其目标,
EventTarget
并且在那里找到的任何事件侦听器都会被触发。 然后,冒泡事件将触发通过向上跟踪EventTarget
父链找到的任何其他事件侦听器,检查在每个连续 EventTarget 上注册的任何事件侦听器。这种向上传播将持续到并包括。Document
事件冒泡为浏览器中的事件委托提供了基础。现在,您可以将事件处理程序绑定到单个父元素,只要该事件发生在其任何子节点(以及它们的任何子节点)上,就会执行该处理程序。这是事件委托。下面是它在实践中的一个例子:
<ul onclick="alert(event.type + '!')">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
在该示例中,如果您单击任何子<li>
节点,您将看到 警告"click!"
,即使<li>
您单击的没有绑定单击处理程序。如果我们绑定onclick="..."
到每个<li>
你会得到相同的效果。
那么有什么好处呢?
想象一下,您现在需要<li>
通过 DOM 操作将新项目动态添加到上述列表中:
var newLi = document.createElement('li');
newLi.innerHTML = 'Four';
myUL.appendChild(newLi);
如果不使用事件委托,您将不得不将"onclick"
事件处理程序“重新绑定”到新<li>
元素,以便它的行为与其兄弟元素相同。使用事件委托,您无需执行任何操作。只需将新添加<li>
到列表中即可。
这对于将事件处理程序绑定到许多元素的 Web 应用程序来说非常棒,其中新元素在 DOM 中动态创建和/或删除。通过事件委托,事件绑定的数量可以通过将它们移动到一个公共父元素来大幅减少,并且动态创建新元素的代码可以与绑定其事件处理程序的逻辑分离。
事件委托的另一个好处是事件侦听器使用的总内存占用减少了(因为事件绑定的数量减少了)。对于经常卸载的小页面(即用户经常导航到不同页面),它可能没有太大区别。但对于长期应用程序来说,它可能很重要。当从 DOM 中删除的元素仍然要求内存(即它们泄漏)时,有一些非常难以追踪的情况,并且这种泄漏的内存通常与事件绑定相关联。使用事件委托,您可以自由地销毁子元素而不会忘记“解除绑定”它们的事件侦听器(因为侦听器位于祖先上)。然后可以包含这些类型的内存泄漏(如果不消除,有时很难做到。IE 我在看着你)。
下面是一些更好的事件委托的具体代码示例:
- JavaScript 事件委托的工作原理
- 事件委托与事件处理
- jQuery.delegate是事件委托+选择器规范
- jQuery.on在将选择器作为第二个参数传递时使用事件委托
- 没有 JavaScript 库的事件委托
- 闭包与事件委托:看看不将代码转换为使用事件委托的优点
- 有趣的方法 PPK 发现委托
focus
和blur
事件(不冒泡)
事件委托允许您避免向特定节点添加事件侦听器;相反,事件侦听器被添加到一个父级。该事件侦听器分析冒泡事件以查找子元素的匹配项。
JavaScript 示例:
假设我们有一个带有多个子元素的父 UL 元素:
<ul id="parent-list">
<li id="post-1">Item 1</li>
<li id="post-2">Item 2</li>
<li id="post-3">Item 3</li>
<li id="post-4">Item 4</li>
<li id="post-5">Item 5</li>
<li id="post-6">Item 6</li>
</ul>
我们也可以说当点击每个子元素时需要发生一些事情。您可以为每个单独的 LI 元素添加一个单独的事件侦听器,但是如果 LI 元素频繁地从列表中添加和删除怎么办?添加和删除事件侦听器将是一场噩梦,尤其是当添加和删除代码位于您的应用程序中的不同位置时。更好的解决方案是向父 UL 元素添加事件侦听器。但是如果将事件侦听器添加到父级,如何知道单击了哪个元素?
简单:当事件冒泡到 UL 元素时,您检查事件对象的目标属性以获得对实际单击节点的引用。这是一个非常基本的 JavaScript 片段,它说明了事件委托:
// Get the element, add a click listener...
document.getElementById("parent-list").addEventListener("click", function(e) {
// e.target is the clicked element!
// If it was a list item
if(e.target && e.target.nodeName == "LI") {
// List item found! Output the ID!
console.log("List item ", e.target.id.replace("post-"), " was clicked!");
}
});
首先向父元素添加一个单击事件侦听器。当事件侦听器被触发时,检查事件元素以确保它是要响应的元素类型。如果是 LI 元素,boom:我们有我们需要的东西!如果它不是我们想要的元素,则可以忽略该事件。这个例子非常简单——UL 和 LI 是一个直接的比较。让我们尝试一些更困难的事情。让我们有一个有很多孩子的父 DIV,但我们只关心一个带有 classA CSS 类的 A 标签:
// Get the parent DIV, add click listener...
document.getElementById("myDiv").addEventListener("click",function(e) {
// e.target was the clicked element
if(e.target && e.target.nodeName == "A") {
// Get the CSS classes
var classes = e.target.className.split(" ");
// Search for the CSS class!
if(classes) {
// For every CSS class the element has...
for(var x = 0; x < classes.length; x++) {
// If it has the CSS class we want...
if(classes[x] == "classA") {
// Bingo!
console.log("Anchor element clicked!");
// Now do something here....
}
}
}
}
});
dom 事件委托与计算机科学的定义有所不同。
它指的是处理来自许多元素(如表格单元格)、来自父对象(如表格)的冒泡事件。它可以使代码更简单,尤其是在添加或删除元素时,并节省一些内存。
委托概念
如果一个父元素中有很多元素,并且您想处理其中的事件 - 不要将处理程序绑定到每个元素。相反,将单个处理程序绑定到其父级,并从 event.target 获取子级。该站点提供了有关如何实现事件委托的有用信息。 http://javascript.info/tutorial/event-delegation
事件委托正在处理使用容器元素上的事件处理程序冒泡的事件,但仅当事件发生在容器内与给定条件匹配的元素上时才激活事件处理程序的行为。这可以简化处理容器内元素的事件。
例如,假设您要处理对大表格中任何表格单元格的单击。您可以编写一个循环来将点击处理程序连接到每个单元格……或者您可以在表格上连接一个点击处理程序并使用事件委托来仅针对表格单元格(而不是表格标题或表格中的空格)触发它在单元格周围排行等)。
当您要从容器中添加和删除元素时,它也很有用,因为您不必担心在这些元素上添加和删除事件处理程序;只需在容器上挂钩事件并在事件冒泡时处理该事件。
这是一个简单的例子(它是故意冗长的以允许内联解释):处理对td
容器表中任何元素的单击:
// Handle the event on the container
document.getElementById("container").addEventListener("click", function(event) {
// Find out if the event targeted or bubbled through a `td` en route to this container element
var element = event.target;
var target;
while (element && !target) {
if (element.matches("td")) {
// Found a `td` within the container!
target = element;
} else {
// Not found
if (element === this) {
// We've reached the container, stop
element = null;
} else {
// Go to the next parent in the ancestry
element = element.parentNode;
}
}
}
if (target) {
console.log("You clicked a td: " + target.textContent);
} else {
console.log("That wasn't a td in the container table");
}
});
table {
border-collapse: collapse;
border: 1px solid #ddd;
}
th, td {
padding: 4px;
border: 1px solid #ddd;
font-weight: normal;
}
th.rowheader {
text-align: left;
}
td {
cursor: pointer;
}
<table id="container">
<thead>
<tr>
<th>Language</th>
<th>1</th>
<th>2</th>
<th>3</th>
</tr>
</thead>
<tbody>
<tr>
<th class="rowheader">English</th>
<td>one</td>
<td>two</td>
<td>three</td>
</tr>
<tr>
<th class="rowheader">Español</th>
<td>uno</td>
<td>dos</td>
<td>tres</td>
</tr>
<tr>
<th class="rowheader">Italiano</th>
<td>uno</td>
<td>due</td>
<td>tre</td>
</tr>
</tbody>
</table>
在进入细节之前,让我们提醒自己 DOM 事件是如何工作的。
DOM 事件从文档分派到目标元素(捕获阶段),然后从目标元素冒泡回文档(冒泡阶段)。旧DOM3 事件规范中的这个图形(现已被取代,但图形仍然有效)显示得非常好:
并非所有事件都会冒泡,但大多数都会冒泡,包括click
.
上面代码示例中的注释描述了它是如何工作的。matches
检查元素是否与 CSS 选择器匹配,当然,如果您不想使用 CSS 选择器,您可以通过其他方式检查某些元素是否与您的条件匹配。
编写该代码是为了详细地调用各个步骤,但是在模糊的现代浏览器上(如果使用 polyfill,也可以在 IE 上),您可以使用closest
andcontains
代替循环:
var target = event.target.closest("td");
console.log("You clicked a td: " + target.textContent);
} else {
console.log("That wasn't a td in the container table");
}
现场示例:
closest
检查您调用它的元素以查看它是否与给定的 CSS 选择器匹配,如果匹配,则返回相同的元素;如果不匹配,则检查父元素是否匹配,如果匹配则返回父元素;如果没有,它会检查父级的父级等。因此它会在祖先列表中找到与选择器匹配的“最近”元素。由于这可能会经过容器元素,因此上面的代码contains
用于检查是否找到了匹配的元素,它是否在容器内——因为通过将事件挂接到容器上,您已经表明您只想处理该容器内的元素.
回到我们的表格示例,这意味着如果您在表格单元格中有一个表格,它将与包含该表格的表格单元格不匹配: