谁能解释一下JavaScript中的事件委托,它是如何有用的?


当前回答

事件委托是使用容器元素上的事件处理程序处理冒泡事件,但只有在事件发生在容器内与给定条件匹配的元素上时才激活事件处理程序的行为。这可以简化容器内元素的事件处理。

例如,假设您想要处理对一个大表格中的任何表格单元格的单击。你可以写一个循环来将一个点击处理程序连接到每个单元格…或者,您可以在表上连接一个单击处理程序,并使用事件委托仅为表单元格触发它(而不是表标题或单元格周围行中的空白等)。

当你要从容器中添加和删除元素时,它也很有用,因为你不必担心在这些元素上添加和删除事件处理程序;只需将事件钩在容器上,并在事件冒泡时处理该事件。

下面是一个简单的例子(为了允许内联解释,它故意很冗长):处理容器表中任意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事件规范(现在已被取代,但图形仍然有效)中的图形非常好地显示了它:

并非所有事件都会冒泡,但大多数都会冒泡,包括点击。

上面代码示例中的注释描述了它是如何工作的。matches检查元素是否与CSS选择器匹配,但当然,如果你不想使用CSS选择器,你可以用其他方式检查是否有元素与你的条件匹配。

这段代码被编写为详细地调用各个步骤,但在模糊的现代浏览器上(如果你使用polyfill,也可以在IE上),你可以使用nearest和contains来代替循环:

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 checks the element you call it on to see if it matches the given CSS selector and, if it does, returns that same element; if not, it checks the parent element to see if it matches, and returns the parent if so; if not, it checks the parent's parent, etc. So it finds the "closest" element in the ancestor list that matches the selector. Since that might go past the container element, the code above uses contains to check that if a matching element was found, it's within the container — since by hooking the event on the container, you've indicated you only want to handle elements within that container.

回到我们的表格例子,这意味着如果你在一个表格单元格中有一个表格,它不会匹配包含表格的表格单元格:

// 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

事件委托是使用容器元素上的事件处理程序处理冒泡事件,但只有在事件发生在容器内与给定条件匹配的元素上时才激活事件处理程序的行为。这可以简化容器内元素的事件处理。

例如,假设您想要处理对一个大表格中的任何表格单元格的单击。你可以写一个循环来将一个点击处理程序连接到每个单元格…或者,您可以在表上连接一个单击处理程序,并使用事件委托仅为表单元格触发它(而不是表标题或单元格周围行中的空白等)。

当你要从容器中添加和删除元素时,它也很有用,因为你不必担心在这些元素上添加和删除事件处理程序;只需将事件钩在容器上,并在事件冒泡时处理该事件。

下面是一个简单的例子(为了允许内联解释,它故意很冗长):处理容器表中任意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事件规范(现在已被取代,但图形仍然有效)中的图形非常好地显示了它:

并非所有事件都会冒泡,但大多数都会冒泡,包括点击。

上面代码示例中的注释描述了它是如何工作的。matches检查元素是否与CSS选择器匹配,但当然,如果你不想使用CSS选择器,你可以用其他方式检查是否有元素与你的条件匹配。

这段代码被编写为详细地调用各个步骤,但在模糊的现代浏览器上(如果你使用polyfill,也可以在IE上),你可以使用nearest和contains来代替循环:

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 checks the element you call it on to see if it matches the given CSS selector and, if it does, returns that same element; if not, it checks the parent element to see if it matches, and returns the parent if so; if not, it checks the parent's parent, etc. So it finds the "closest" element in the ancestor list that matches the selector. Since that might go past the container element, the code above uses contains to check that if a matching element was found, it's within the container — since by hooking the event on the container, you've indicated you only want to handle elements within that container.

回到我们的表格例子,这意味着如果你在一个表格单元格中有一个表格,它不会匹配包含表格的表格单元格:

// 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>

要理解事件委托,首先我们需要知道为什么以及什么时候需要或想要事件委托。

可能有很多情况,但让我们讨论事件委托的两个主要用例。 1. 第一种情况是当一个元素有很多感兴趣的子元素时。在本例中,我们不向所有这些子元素添加事件处理程序,而是将其添加到父元素,然后确定在哪个子元素上触发事件。

2.事件委托的第二个用例是当我们想要将事件处理程序附加到加载页面时还不在DOM中的元素时。当然,这是因为我们不能将事件处理程序添加到不在页面上的东西,所以在不赞成的情况下,我们正在编写代码。

假设在加载页面时,DOM中有一个包含0、10或100个条目的列表,并且还有更多条目等待添加到列表中。因此,没有办法为未来的元素附加事件处理程序,或者这些元素还没有添加到DOM中,而且可能有很多项,因此每个项都附加一个事件处理程序是没有用的。

事件的代表团

为了讨论事件委托,我们需要讨论的第一个概念是事件冒泡。

事件冒泡: 事件冒泡意味着当某个DOM元素上触发或触发事件时,例如通过单击下面图像上的按钮,那么所有父元素上也会触发完全相同的事件。

事件首先在按钮上触发,但随后它也会在所有父元素上触发,因此它也会在段落到section的主元素上触发,实际上在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);
    }

事件委托利用了JavaScript事件中两个经常被忽视的特性:事件冒泡和目标元素。当一个事件在一个元素上被触发时,例如鼠标点击一个按钮,同样的事件也会在该元素的所有祖先上被触发。这个过程被称为事件冒泡;事件从初始元素冒泡到DOM树的顶部。

Imagine an HTML table with 10 columns and 100 rows in which you want something to happen when the user clicks on a table cell. For example, I once had to make each cell of a table of that size editable when clicked. Adding event handlers to each of the 1000 cells would be a major performance problem and, potentially, a source of browser-crashing memory leaks. Instead, using event delegation, you would add only one event handler to the table element, intercept the click event and determine which cell was clicked.

事件的代表团

将事件监听器附加到父元素,当子元素上发生事件时触发该监听器。

事件传播

当事件通过DOM从子元素移动到父元素时,这称为事件传播,因为事件在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>

Codepen