什么是DOM事件委托?

268

有人能否解释一下JavaScript中的事件委托是什么以及它的作用是什么?


3
如果有一个有用的信息源链接会很不错。到目前为止,在Google搜索“dom event delegation”时,这是排名第一的结果。也许这个链接有用?我不是完全确定:http://www.w3.org/TR/DOM-Level-2-Events/events.html - Sean McMillan
或者是这个:http://www.sitepoint.com/blogs/2008/07/23/javascript-event-delegation-is-easier-than-you-think/ - Sean McMillan
9
这是一个流行的内容。即使是 Facebook 的人也会在他们的 ReactJS 页面上链接到这个 https://davidwalsh.name/event-delegate - Sorter
看一下这个链接 https://javascript.info/event-delegation,它会对你有很大帮助。 - Suraj Jain
10个回答

409

DOM事件委托是一种通过单个公共父级而不是每个子级响应UI事件的机制,通过事件“冒泡”(也称为事件传播)实现。

当元素上触发事件时,会发生以下情况

事件被分派到其目标EventTarget,并触发任何在那里找到的事件侦听器。冒泡事件将继续跟随EventTarget的父级链向上,检查每个连续的EventTarget上注册的任何事件侦听器。这种向上的传播将持续到包括Document在内。

事件冒泡为浏览器中的事件委托提供了基础。现在,您可以将事件处理程序绑定到单个父元素,并且无论事件发生在其任何子节点上(以及它们的任何子节点),该处理程序都将被执行。 这就是事件委托。 以下是实际示例:

<ul onclick="alert(event.type + '!')">
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
</ul>

我举个例子,如果你点击任何一个子节点<li>,你会看到一个弹窗显示"click!",即使没有绑定到你点击的<li>上。如果我们给每个<li>绑定onclick="...",你也会得到同样的效果。

那么好处是什么?

现在想象一下,你需要通过DOM操作动态添加新的<li>项到上面的列表中:

var newLi = document.createElement('li');
newLi.innerHTML = 'Four';
myUL.appendChild(newLi);

不使用事件委托,您将不得不重新绑定“onclick”事件处理程序到新的
  • 元素,以使其与其兄弟元素的行为相同。使用事件委托,您无需做任何事情。只需将新的
  • 添加到列表中即可。

    对于绑定到许多元素的事件处理程序的Web应用程序,其中在DOM中动态创建和/或删除新元素,这绝对是一个好消息。使用事件委托,可以通过将它们移动到共同的父元素来大大减少事件绑定的数量,并且可以将动态创建新元素的代码与绑定其事件处理程序的逻辑解耦。

    事件委托的另一个好处是事件监听器使用的总内存占用量会降低(因为事件绑定的数量会降低)。对于经常卸载的小页面(即用户经常导航到不同的页面),可能没有太大区别。但对于长时间运行的应用程序,这可能是重要的。有一些非常难以跟踪的情况,当从DOM中删除元素时,它们仍然占用内存(即泄漏),并且通常这种泄漏的内存与事件绑定有关。使用事件委托,您可以自由地销毁子元素,而不必担心忘记“解除绑定”其事件侦听器(因为侦听器在祖先上)。这些类型的内存泄漏可以得到控制(如果不是消除,有时很难做到。IE我正在看着你)。

    以下是事件委托的一些更好的具体代码示例:


  • 我在打开你的第三个链接“不使用JavaScript库的事件委托”时遇到了访问被禁止的问题,同时对你的最后一个链接点赞。 - bugwheels94
    你好,感谢您提供的出色解释。然而,我仍然对某些细节感到困惑:根据我理解的DOM树事件流程(可以在3.1.事件分发和DOM事件流中看到),事件对象会传播直到达到目标元素,然后再冒泡。如果这个节点的父节点是事件的目标元素,那么它怎么能够到达该节点的子元素呢?例如,当应该在<ul>停止时,事件如何传播到<li>?如果我的问题还不清楚或需要单独的讨论,请告诉我。 - Imad
    @Aetos:*> 如果这个节点的父级是事件目标,它怎么能够到达该节点的子元素?* 据我所知,它是不可能的。事件在第一阶段(捕获)结束于目标的父级,在目标本身进入第二阶段(目标),然后从目标的父级开始进入第三阶段(冒泡)。在任何地方都不会到达目标的子元素。 - Crescent Fresh
    @Crescent Fresh 那么如果事件从未到达子节点,它又如何应用于子节点呢? - Imad
    1
    非常棒的回答。感谢您用相关事实解释事件委托。谢谢! - Kshitij
    显示剩余5条评论

    51

    事件委托允许您避免将事件监听器添加到特定节点;相反,事件监听器添加到一个父节点。该事件监听器分析冒泡事件以在子元素上找到匹配项。

    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....
                    }
                }
            }
        }
    });
    

    http://davidwalsh.name/event-delegate


    5
    建议修改:在最后一个例子中使用 e.classList.contains() 替代现有的方法。参考链接:https://developer.mozilla.org/en-US/docs/Web/API/Element/classList - nc.

    9

    DOM事件委托与计算机科学定义不同。

    它指的是从许多元素(如表格单元格)处理冒泡事件,而不是从父对象(如表格)中处理。这可以使代码更简单,特别是在添加或删除元素时,并且节省一些内存。


    8
    事件委托是使用容器元素上的事件处理程序处理冒泡事件,但仅在发生在容器内与给定条件匹配的元素上时才激活事件处理程序的行为。这可以简化容器内元素的事件处理。
    例如,假设您想处理大表中任何表格单元格的点击。您可以编写循环来将点击处理程序挂钩到每个单元格...或者您可以在表格上挂钩一个点击处理程序,并使用事件委托仅触发表格单元格的处理程序(而不是表头或围绕单元格周围的行中的空格等)。
    当您要从容器中添加和删除元素时,它也非常有用,因为您不必担心在这些元素上添加和删除事件处理程序;只需在容器上挂接事件并在冒泡时处理事件即可。
    以下是一个简单的示例(它故意冗长以便进行内联说明):处理容器表格中任何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事件规范(现已被取代,但该图仍然有效),非常清晰地展示了它:

    enter image description here

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

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

    该代码被编写为详细调用各个步骤,但在近代浏览器(如果使用 polyfill,则也适用于 IE)上,您可以使用 closestcontains 替代循环:

    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>


    8
    要理解事件委托,首先我们需要知道为什么和何时需要或希望使用事件委托。
    可能有很多情况,但让我们讨论两个重要的事件委托用例。 1. 第一种情况是当我们有一个包含许多我们感兴趣的子元素的元素时。在这种情况下,我们不是将事件处理程序添加到所有这些子元素上,而是将其添加到父元素上,然后确定事件发生在哪个子元素上。
    2. 事件委托的第二个用例是当我们想要将事件处理程序附加到尚未在页面加载时出现在DOM中的元素时。那当然是因为我们无法向不在我们页面上的东西添加事件处理程序,在我们编写代码时会出现这种情况。
    假设您在加载页面时在DOM中有0、10或100个项目列表,并且还有更多的项目等待添加到列表中。因此,没有办法为未来的元素或尚未添加到DOM中的元素附加事件处理程序,而且可能有很多项目,因此将一个事件处理程序附加到每个项目上是没有用的。
    事件委托
    好的,为了谈论事件委托,我们首先需要讨论的概念是事件冒泡。
    事件冒泡: 事件冒泡意味着当某个DOM元素上触发一个事件,例如在下面图片中单击按钮时,同样的事件也会在所有父元素上触发。

    enter image description here

    事件首先在按钮上触发,但随后它也会依次在所有父元素上触发,因此它也会在段落、主元素所处的区域以及DOM树中一直向上触发,直到根元素HTML。所以我们说事件在DOM树内部冒泡,这就是为什么它被称为冒泡的原因。

    1 2 3 4

    目标元素:事件最初触发的元素称为目标元素,也就是导致事件发生的元素。在上面的例子中,当然是被点击的按钮。重要的是,这个目标元素作为事件对象的一个属性被存储起来。这意味着所有父元素也会知道事件的目标元素,即事件最初触发的位置。 这就引出了事件委托:如果事件在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);
        }
    

    7

    委托模式

    如果一个父元素内有多个子元素,你想对它们进行事件处理 - 不要给每个元素绑定处理程序。 相反,将单个处理程序绑定到它们的父元素,并从event.target获取子元素。 本站提供有关如何实现事件委托的有用信息。 http://javascript.info/tutorial/event-delegation


    6

    委托是一种技术,其中对象向外部表达某些行为,但实际上将实现该行为的责任委托给一个相关联的对象。这听起来与代理模式非常相似,但其目的却完全不同。委托是一种抽象机制,可集中对象(方法)行为。

    通俗地说:使用委托作为继承的替代方案。当父子对象之间存在紧密关系时,继承是一种很好的策略,但继承会使对象之间耦合得非常紧密。通常,委托是表达类之间关系的更灵活方式。

    该模式也称为“代理链”。其他几个设计模式使用委托-状态、策略和访问者模式都依赖于它。


    1
    好的解释。在具有多个<li>子元素的<ul>示例中,显然<li>处理单击逻辑,但实际上并非如此,因为它们将此逻辑“委托”给父<ul>。 - Juanma Menendez

    2
    事件委托利用了JavaScript事件中常被忽视的两个特性:事件冒泡和目标元素。当在元素上触发事件时,例如在按钮上单击鼠标,相同的事件也会在该元素的所有祖先元素上触发。这个过程被称为事件冒泡;事件从起源元素冒泡到DOM树的顶部。
    想象一个有10列和100行的HTML表格,在其中当用户点击表格单元格时,你希望发生某些事情。例如,我曾经需要使这样大小的表格中的每个单元格都可以编辑。为每个单元格添加事件处理程序将是一个主要的性能问题,并且可能是浏览器崩溃内存泄漏的来源。相反,使用事件委托,您只需向表格元素添加一个事件处理程序,拦截点击事件并确定点击了哪个单元格。

    2

    这基本上是关于如何将关联与元素进行匹配。 .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将不遵守点击事件,因此它将不会被移除。

    如果它是通过 .on 与选择器分离进行关联的,它将遵守。


    这个答案是错误的。根据jQuery文档中onclick的说明,两者的行为是相同的,后者只是一种简写方式。在您的代码示例中,您选择了不同的目标,因此会得到不同的行为。 另外,您的答案特别涉及jQuery,但这一事实从未被提及。 - Martin

    0
    事件代理

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

    事件传播

    当事件从子元素经过 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

    {{链接1:Codepen}}


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接