什么是DOM事件委托?

时间:2009-11-06 12:33:49

标签: javascript javascript-events event-handling event-delegation

任何人都可以在JavaScript中解释事件委托以及它有用吗?

11 个答案:

答案 0 :(得分:278)

DOM事件委托是一种通过事件“冒泡”(又称事件传播)通过单个共同父母而不是每个孩子来响应ui事件的机制。

当元素触发事件时,the following occurs

  

将事件分派到目标   EventTarget和任何事件监听器   发现有触发。 鼓泡   事件将触发任何事件   找到的其他事件监听器   跟随EventTarget的父母   链向上,检查任何事件   每个人都注册了听众   连续的EventTarget。这个向上   传播将持续到和   包括Document

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

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

使用该示例,如果您单击任何子<li>个节点,即使没有绑定到"click!"的点击处理程序,您也会看到<li>的警报你点击了。如果我们将onclick="..."绑定到每个<li>,您将获得相同的效果。

那么有什么好处?

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

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

如果没有使用事件委派,您必须将"onclick"事件处理程序“重新绑定”到新的<li>元素,以使其行为与其相同的方式兄弟姐妹。 使用事件委派,您无需执行任何操作。只需将新的<li>添加到列表中即可完成。

对于具有绑定到许多元素的事件处理程序的Web应用程序来说,这绝对是太棒了,在DOM中动态创建和/或删除新元素。通过事件委托,可以通过将事件绑定移动到公共父元素来大幅减少事件绑定的数量,并且动态创建新元素的代码可以与绑定其事件处理程序的逻辑分离。

事件委派的另一个好处是事件侦听器使用的总内存占用量下降(因为事件绑定的数量会下降)。对于经常卸载的小页面(即用户经常导航到不同页面)可能没有多大区别。但对于长期存在的应用程序来说,它可能很重要。当从DOM中删除的元素仍然声称存储器(即它们泄漏)时,存在一些非常难以跟踪的情况,并且这种泄漏的存储器通常与事件绑定相关联。通过事件委派,您可以自由地销毁子元素而不会忘记“解除绑定”他们的事件监听器(因为监听器位于祖先上)。然后可以包含这些类型的内存泄漏(如果没有消除,有时候很难做到.IE我在看着你。)

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

答案 1 :(得分:26)

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

JavaScript示例:

让我们说我们有一个带有多个子元素的父UL元素:

// 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元素添加单独的事件侦听器,但是如果经常在列表中添加和删除LI元素会怎么样?添加和删​​除事件侦听器将是一场噩梦,尤其是如果添加和删除代码位于应用程序中的不同位置。更好的解决方案是将事件侦听器添加到父UL元素。但是,如果您将事件侦听器添加到父级,您将如何知道单击了哪个元素?

简单:当事件冒泡到UL元素时,检查事件对象的目标属性以获得对实际单击节点的引用。这是一个非常基本的JavaScript代码段,用于说明事件委派:

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

  }
});

首先将click事件侦听器添加到父元素。触发事件侦听器时,请检查事件元素以确保它是要响应的元素类型。如果它是一个LI元素,繁荣:我们有我们需要的东西!如果它不是我们想要的元素,则可以忽略该事件。这个例子非常简单 - UL和LI是一个直接的比较。让我们尝试更难的事情。让我们有一个有很多孩子的父DIV,但我们关心的是一个带有classA CSS类的A标签:

{{1}}

http://davidwalsh.name/event-delegate

答案 2 :(得分:7)

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

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

答案 3 :(得分:6)

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

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

此模式也称为“代理链”。其他几种设计模式使用委托 - 状态,策略和访客模式依赖于它。

答案 4 :(得分:5)

代表团概念

如果一个父级中有许多元素,并且您希望处理它们上的事件 - 请不要将处理程序绑定到每个元素。 相反,将单个处理程序绑定到其父级,并从event.target获取子级。 该站点提供有关如何实现事件委派的有用信息。 http://javascript.info/tutorial/event-delegation

答案 5 :(得分: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>

.Click事件:

$("li").click(function () {
   $(this).remove ();
});

事件.on:

$("#todo").on("click", "li", function () {
   $(this).remove();
});

请注意,我已经在.on中分离了选择器。我会解释原因。

让我们假设在这种关联之后,让我们执行以下操作:

$("#todo").append("<li>Do 5</li>");

那是您会注意到差异的地方。

如果该事件是通过.click关联的,则任务5将不会遵守click事件,因此不会将其删除。

如果它是通过.on关联的,则选择器分开,它将服从。

答案 6 :(得分:2)

事件委托正在使用容器元素上的事件处理程序来处理冒泡的事件,但仅在事件发生在符合给定条件的容器内某个元素上时,才激活事件处理程序的行为。这样可以简化处理容器内元素的事件。

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

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

这是一个简单的示例(故意进行冗长的解释以进行内联说明):处理对容器表中任何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 events spec中的此图形(现已取代,但该图形仍然有效)显示得很好:

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>

答案 7 :(得分:2)

要首先了解事件委托,我们需要知道为什么以及何时真正需要或想要事件委托。

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

2。事件委托的第二个用例是当我们希望在加载页面时将事件处理程序附加到尚未在DOM中的元素上。当然,那是因为我们不能向不在页面上的内容添加事件处理程序,因此在不赞成使用的情况下,我们正在编码。

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

事件委托

好的,因此,为了谈论事件委托,我们实际上需要谈论的第一个概念是事件冒泡。

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

enter image description here

该事件首先在按钮上触发,但是随后也会一次在所有父元素上触发该事件,因此它还将在主元素部分的段落上触发,实际上一直在直到作为根的HTML元素为止的DOM树。因此,我们说该事件在DOM树中冒泡,这就是为什么将其称为冒泡的原因。

1 2 3 4

目标元素:实际上首先在其上触发事件的元素称为目标元素,因此导致事件发生的元素称为目标元素。在上面的示例中,当然是单击的按钮。重要的部分是此目标元素作为属性存储在事件对象中,这意味着将在其上触发该事件的所有父元素都将知道该事件的目标元素,因此该事件是在第一时间触发的。 / p>

这使我们进入事件委托,因为如果事件在DOM树中冒泡,并且如果我们知道事件在何处触发,则只需将事件处理程序附加到父元素上,然后等待让事件冒泡,然后我们可以对目标元素进行任何打算做的事情。此技术称为事件委托。在此示例中,我们可以简单地添加事件处理程序 到主要元素。

好的,事件委托不是在我们感兴趣的原始元素上设置事件处理程序,而是将其附加到父元素上,并基本上在那儿捕获事件,因为它冒泡了。然后,我们可以使用target element属性对我们感兴趣的元素进行操作。

示例: 现在,假设我们的页面中有两个列表项,在以编程方式添加这些列表项之后,我们要从其中删除一个或多个项目。使用事件委托技术,我们可以轻松实现目标。

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

答案 8 :(得分:1)

C#中的委托类似于C或C ++中的函数指针。使用委托允许程序员封装对委托对象内的方法的引用。然后可以将委托对象传递给可以调用引用方法的代码,而无需在编译时知道将调用哪个方法。

查看此链接 - &gt; http://www.akadia.com/services/dotnet_delegates_and_events.html

答案 9 :(得分:1)

事件委托利用了JavaScript事件的两个经常被忽略的特征:事件冒泡和目标元素。当在元素上触发事件时(例如,鼠标单击按钮时),所有该元素的祖先。此过程称为事件冒泡;事件从原始元素冒泡到DOM树的顶部。

想象一下一个10列100行的HTML表格,当用户单击表格单元格时,您希望在其中进行某些操作。例如,我曾经不得不在单击时使该大小的表的每个单元格可编辑。向1000个单元中的每个单元添加事件处理程序将是一个主要的性能问题,并且可能是导致浏览器崩溃的内存泄漏的源头。相反,使用事件委托,您将只向表元素添加一个事件处理程序,拦截click事件并确定单击了哪个单元格。

答案 10 :(得分: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