谁能解释一下 JavaScript 中的事件委托以及它有什么用处?
11 回答
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 元素,繁荣:我们有我们需要的东西!如果它不是我们想要的元素,则可以忽略该事件。这个例子非常简单——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 事件委托与计算机科学定义不同。
它指的是处理来自父对象(如表格)的许多元素(如表格单元格)的冒泡事件。它可以使代码更简单,尤其是在添加或删除元素时,并节省一些内存。
事件委托是使用容器元素上的事件处理程序处理冒泡的事件,但仅当事件发生在容器内符合给定条件的元素上时才激活事件处理程序的行为。这可以简化容器内元素的事件处理。
例如,假设您要处理对大表格中任何表格单元格的单击。您可以编写一个循环来将点击处理程序连接到每个单元格......或者您可以在表格上连接一个点击处理程序并使用事件委托仅针对表格单元格(而不是表格标题或表格中的空格)触发它在单元格周围排等)。
当您要从容器中添加和删除元素时,它也很有用,因为您不必担心在这些元素上添加和删除事件处理程序;只需将事件挂在容器上并在它冒泡时处理事件。
下面是一个简单的例子(它故意冗长以允许内联解释):处理对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");
}
现场示例:
// Handle the event on the container
document.getElementById("container").addEventListener("click", function(event) {
var target = event.target.closest("td");
if (target && this.contains(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>
closest
检查你调用它的元素,看它是否匹配给定的 CSS 选择器,如果匹配,则返回相同的元素;如果不匹配,则检查父元素是否匹配,如果匹配则返回父元素;如果不是,它会检查父级的父级等。因此它会在祖先列表中找到与选择器匹配的“最近”元素。由于这可能会超出容器元素,因此上面的代码contains
用于检查是否找到匹配元素,它是否在容器内 - 因为通过在容器上挂钩事件,您已经表明您只想处理该容器内的元素.
回到我们的表格示例,这意味着如果表格单元格中有一个表格,它将与包含该表格的表格单元格不匹配:
// Handle the event on the container
document.getElementById("container").addEventListener("click", function(event) {
var target = event.target.closest("td");
if (target && this.contains(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;
}
<!-- The table wrapped around the #container table -->
<table>
<tbody>
<tr>
<td>
<!-- This cell doesn't get matched, thanks to the `this.contains(target)` check -->
<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>
</td>
<td>
This is next to the container table
</td>
</tr>
</tbody>
</table>
委托是一种技术,其中对象向外部表达某些行为,但实际上将实现该行为的责任委托给相关对象。这听起来与代理模式非常相似,但它的用途却大不相同。委托是一种集中对象(方法)行为的抽象机制。
一般而言:使用委托作为继承的替代方案。继承是一种很好的策略,当父对象和子对象之间存在密切关系时,继承将对象耦合得非常紧密。通常,委托是表达类之间关系的更灵活的方式。
这种模式也称为“代理链”。其他几种设计模式使用委托——状态、策略和访问者模式都依赖于它。
委托理念
如果一个父元素中有许多元素,并且您想处理它们的事件 - 不要将处理程序绑定到每个元素。相反,将单个处理程序绑定到其父级,并从 event.target 获取子级。该站点提供了有关如何实现事件委托的有用信息。 http://javascript.info/tutorial/event-delegation
要首先了解事件委托,我们需要知道我们真正需要或想要事件委托的原因和时间。
可能有很多情况,但让我们讨论一下事件委托的两个大用例。1. 第一种情况是当我们有一个包含很多我们感兴趣的子元素的元素时。在这种情况下,我们不向所有这些子元素添加事件处理程序,而是简单地将其添加到父元素,然后确定触发事件的子元素。
2. 事件委托的第二个用例是当我们希望在页面加载时将事件处理程序附加到尚未在 DOM 中的元素。当然,这是因为我们无法将事件处理程序添加到不在我们页面上的内容,所以在不推荐使用的情况下,我们正在编码。
假设您在加载页面时在 DOM 中有一个包含 0、10 或 100 个项目的列表,并且还有更多项目正在等待您添加到列表中。所以没有办法为将来的元素附加事件处理程序,或者这些元素还没有添加到 DOM 中,而且可能有很多项目,所以给每个元素附加一个事件处理程序是没有用的其中。
事件委托
好的,所以为了谈论事件委托,我们真正需要谈论的第一个概念是事件冒泡。
事件冒泡: 事件冒泡意味着当某个 DOM 元素上触发或触发事件时,例如通过单击下面图像上的按钮,那么完全相同的事件也会在所有父元素上触发。
该事件首先在按钮上触发,但随后它也会一次在所有父元素上触发,因此它还将在段落到主要元素的部分上触发,实际上一直在 DOM 树中直到作为根的 HTML 元素。所以我们说事件在 DOM 树中冒泡,这就是为什么它被称为冒泡。
目标元素:实际上第一次触发事件的元素称为目标元素,因此导致事件发生的元素称为目标元素。在我们上面的例子中,它当然是被点击的按钮。重要的部分是该目标元素作为属性存储在事件对象中,这意味着所有将触发该事件的父元素都将知道该事件的目标元素,即事件首先触发的位置。
这给我们带来了事件委托,因为如果事件在 DOM 树中冒泡,并且如果我们知道事件被触发的位置,那么我们可以简单地将事件处理程序附加到父元素并等待事件冒泡,我们可以然后对目标元素做任何我们想要做的事情。这种技术称为事件委托。在此示例中,我们可以简单地将事件处理程序添加到主元素。
好吧,同样,事件委托不是在我们感兴趣的原始元素上设置事件处理程序,而是将它附加到父元素,并且基本上,在那里捕获事件,因为它会冒泡。然后,我们可以使用目标元素属性对我们感兴趣的元素进行操作。
示例: 现在假设我们的页面中有两个列表项,在以编程方式在这些列表中添加项目之后,我们希望从中删除一个或多个项目。使用事件委托技术,我们可以轻松实现我们的目的。
<div class="body">
<div class="top">
</div>
<div class="bottom">
<div class="other">
<!-- other bottom elements -->
</div>
<div class="container clearfix">
<div class="income">
<h2 class="icome__title">Income</h2>
<div class="income__list">
<!-- list items -->
</div>
</div>
<div class="expenses">
<h2 class="expenses__title">Expenses</h2>
<div class="expenses__list">
<!-- list items -->
</div>
</div>
</div>
</div>
</div>
在这些列表中添加项目:
const DOMstrings={
type:{
income:'inc',
expense:'exp'
},
incomeContainer:'.income__list',
expenseContainer:'.expenses__list',
container:'.container'
}
var addListItem = function(obj, type){
//create html string with the place holder
var html, element;
if(type===DOMstrings.type.income){
element = DOMstrings.incomeContainer
html = `<div class="item clearfix" id="inc-${obj.id}">
<div class="item__description">${obj.descripiton}</div>
<div class="right clearfix">
<div class="item__value">${obj.value}</div>
<div class="item__delete">
<button class="item__delete--btn"><i class="ion-ios-close-outline"></i></button>
</div>
</div>
</div>`
}else if (type ===DOMstrings.type.expense){
element=DOMstrings.expenseContainer;
html = ` <div class="item clearfix" id="exp-${obj.id}">
<div class="item__description">${obj.descripiton}</div>
<div class="right clearfix">
<div class="item__value">${obj.value}</div>
<div class="item__percentage">21%</div>
<div class="item__delete">
<button class="item__delete--btn"><i class="ion-ios-close-outline"></i></button>
</div>
</div>
</div>`
}
var htmlObject = document.createElement('div');
htmlObject.innerHTML=html;
document.querySelector(element).insertAdjacentElement('beforeend', htmlObject);
}
删除项目:
var ctrlDeleteItem = function(event){
// var itemId = event.target.parentNode.parentNode.parentNode.parentNode.id;
var parent = event.target.parentNode;
var splitId, type, ID;
while(parent.id===""){
parent = parent.parentNode
}
if(parent.id){
splitId = parent.id.split('-');
type = splitId[0];
ID=parseInt(splitId[1]);
}
deleteItem(type, ID);
deleteListItem(parent.id);
}
var deleteItem = function(type, id){
var ids, index;
ids = data.allItems[type].map(function(current){
return current.id;
});
index = ids.indexOf(id);
if(index>-1){
data.allItems[type].splice(index,1);
}
}
var deleteListItem = function(selectorID){
var element = document.getElementById(selectorID);
element.parentNode.removeChild(element);
}
它基本上是如何与元素建立关联的。.click
适用于当前 DOM,而.on
(使用委托)将继续对事件关联后添加到 DOM 的新元素有效。
哪个更好用,我会说这取决于具体情况。
例子:
<ul id="todo">
<li>Do 1</li>
<li>Do 2</li>
<li>Do 3</li>
<li>Do 4</li>
</ul>
.点击事件:
$("li").click(function () {
$(this).remove ();
});
事件.on:
$("#todo").on("click", "li", function () {
$(this).remove();
});
请注意,我已经在 .on 中分离了选择器。我会解释为什么。
让我们假设在这个关联之后,让我们执行以下操作:
$("#todo").append("<li>Do 5</li>");
这就是您会注意到差异的地方。
如果事件是通过 .click 关联的,则任务 5 将不服从 click 事件,因此不会被删除。
如果它是通过 .on 关联的,并且选择器是分开的,它将服从。
C# 中的委托类似于 C 或 C++ 中的函数指针。使用委托允许程序员在委托对象内封装对方法的引用。然后可以将委托对象传递给可以调用引用方法的代码,而不必在编译时知道将调用哪个方法。
请参阅此链接 --> http://www.akadia.com/services/dotnet_delegates_and_events.html
事件委托利用了 JavaScript 事件的两个经常被忽视的特性:事件冒泡和目标元素。当在元素上触发事件时,例如鼠标单击按钮,同样的事件也会在该元素的所有祖先上触发. 这个过程称为事件冒泡;事件从原始元素冒泡到 DOM 树的顶部。
想象一个包含 10 列和 100 行的 HTML 表格,当用户单击表格单元格时,您希望在其中发生一些事情。例如,我曾经不得不使该大小的表格的每个单元格在单击时都可编辑。为 1000 个单元中的每一个添加事件处理程序将是一个主要的性能问题,并且可能是导致浏览器崩溃的内存泄漏的来源。相反,使用事件委托,您只需向表格元素添加一个事件处理程序,拦截单击事件并确定单击了哪个单元格。
将事件侦听器附加到父元素,当子元素发生事件时触发该事件侦听器。
事件传播当事件通过 DOM 从子元素移动到父元素时,这称为Event Propagation,因为事件会传播或移动通过 DOM。
在此示例中,来自按钮的事件 (onclick) 被传递到父段落。
$(document).ready(function() {
$(".spoiler span").hide();
/* add event onclick on parent (.spoiler) and delegate its event to child (button) */
$(".spoiler").on( "click", "button", function() {
$(".spoiler button").hide();
$(".spoiler span").show();
} );
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<p class="spoiler">
<span>Hello World</span>
<button>Click Me</button>
</p>