2022 年 2 月 3 日

事件委托

捕获和冒泡使我们能够实现一种称为事件委托的最强大的事件处理模式之一。

其思想是,如果我们有很多以类似方式处理的元素,那么我们不会为每个元素分配一个处理程序,而是为它们的公共祖先放置一个处理程序。

在处理程序中,我们获取 event.target 来查看事件实际发生的位置并处理它。

让我们看一个示例 - 八卦图 反映了古代中国哲学。

它在这里

HTML 如下所示

<table>
  <tr>
    <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="n">...</td>
    <td class="ne">...</td>
  </tr>
  <tr>...2 more lines of this kind...</tr>
  <tr>...2 more lines of this kind...</tr>
</table>

表格有 9 个单元格,但可以有 99 个或 9999 个,这并不重要。

我们的任务是单击时突出显示一个单元格 <td>

我们不会为每个 <td>(可能很多)分配一个 onclick 处理程序 - 我们将在 <table> 元素上设置“catch-all”处理程序。

它将使用 event.target 来获取单击的元素并突出显示它。

代码

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // where was the click?

  if (target.tagName != 'TD') return; // not on TD? Then we're not interested

  highlight(target); // highlight it
};

function highlight(td) {
  if (selectedTd) { // remove the existing highlight if any
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // highlight the new td
}

这样的代码不关心表格中有多少个单元格。我们可以随时动态添加/删除 <td>,突出显示仍然有效。

不过,有一个缺点。

单击可能不会发生在 <td> 上,而是在其内部。

在我们的案例中,如果我们查看 HTML 内部,我们可以看到 <td> 内部的嵌套标签,例如 <strong>

<td>
  <strong>Northwest</strong>
  ...
</td>

自然地,如果单击发生在该 <strong> 上,那么它将成为 event.target 的值。

在处理程序 table.onclick 中,我们应该获取这样的 event.target 并找出单击是否在 <td> 内部。

以下是改进后的代码

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

解释

  1. 方法 elem.closest(selector) 返回与选择器匹配的最近祖先。在我们的案例中,我们从源元素向上查找 <td>
  2. 如果 event.target 不在任何 <td> 内,则调用会立即返回,因为没有事情可做。
  3. 在嵌套表格的情况下,event.target 可能是一个 <td>,但位于当前表格之外。因此,我们检查它是否实际上是我们的表格的 <td>
  4. 如果是,则突出显示它。

结果,我们有了一个快速、高效的突出显示代码,它不关心表格中 <td> 的总数。

委托示例:标记中的操作

事件委托还有其他用途。

假设我们要制作一个带有按钮“保存”、“加载”、“搜索”等的菜单。并且有一个带有方法 saveloadsearch…的对象,如何匹配它们?

第一个想法可能是为每个按钮分配一个单独的处理程序。但有一个更优雅的解决方案。我们可以为整个菜单添加一个处理程序,并为具有要调用的方法的按钮添加 data-action 属性

<button data-action="save">Click to Save</button>

处理程序读取属性并执行方法。看看工作示例

<div id="menu">
  <button data-action="save">Save</button>
  <button data-action="load">Load</button>
  <button data-action="search">Search</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('saving');
    }

    load() {
      alert('loading');
    }

    search() {
      alert('searching');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action]();
      }
    };
  }

  new Menu(menu);
</script>

请注意,this.onClick(*) 中绑定到 this。这很重要,因为否则其中的 this 将引用 DOM 元素(elem),而不是 Menu 对象,并且 this[action] 将不是我们需要的。

那么,委托在这里给我们带来了什么好处?

  • 我们不需要编写代码为每个按钮分配一个处理程序。只需创建一个方法并将其放入标记中。
  • HTML 结构是灵活的,我们可以随时添加/删除按钮。

我们还可以使用类 .action-save.action-load,但属性 data-action 在语义上更好。我们还可以在 CSS 规则中使用它。

“行为”模式

我们还可以使用事件委托以声明方式向元素添加“行为”,使用特殊属性和类。

该模式有两部分

  1. 我们向一个元素添加一个自定义属性来描述其行为。
  2. 一个文档范围的处理程序跟踪事件,如果事件发生在带属性的元素上——执行该操作。

行为:计数器

例如,这里属性 data-counter 为按钮添加了一个行为:“点击时增加值”

Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // if the attribute exists...
      event.target.value++;
    }

  });
</script>

如果我们点击一个按钮——它的值就会增加。不是按钮,但这里重要的是通用方法。

可以有任意多个带有 data-counter 的属性。我们可以随时向 HTML 中添加新的属性。使用事件委托,我们“扩展”了 HTML,添加了一个描述新行为的属性。

对于文档级别的处理程序——始终是 addEventListener

当我们为 document 对象分配一个事件处理程序时,我们应该始终使用 addEventListener,而不是 document.on<event>,因为后者会导致冲突:新处理程序会覆盖旧处理程序。

对于实际项目来说,由代码的不同部分设置的 document 上有很多处理程序是正常的。

行为:切换器

另一个行为示例。点击具有属性 data-toggle-id 的元素将显示/隐藏具有给定 id 的元素

<button data-toggle-id="subscribe-mail">
  Show the subscription form
</button>

<form id="subscribe-mail" hidden>
  Your mail: <input type="email">
</form>

<script>
  document.addEventListener('click', function(event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>

让我们再次注意我们所做的。现在,要向元素添加切换功能——不需要了解 JavaScript,只需使用属性 data-toggle-id

这可能会变得非常方便——无需为每个此类元素编写 JavaScript。只需使用该行为。文档级别的处理程序使其适用于页面的任何元素。

我们还可以在单个元素上组合多个行为。

“行为”模式可以作为 JavaScript 微片段的替代方案。

摘要

事件委托非常酷!这是用于 DOM 事件的最有用的模式之一。

它通常用于为许多类似的元素添加相同的处理,但不仅限于此。

算法

  1. 在容器上放置一个单独的处理程序。
  2. 在处理程序中 - 检查源元素 event.target
  3. 如果事件发生在我们感兴趣的元素内部,则处理该事件。

优点

  • 简化初始化并节省内存:无需添加许多处理程序。
  • 代码更少:添加或删除元素时,无需添加/删除处理程序。
  • DOM 修改:我们可以使用 innerHTML 等批量添加/删除元素。

当然,委托有其局限性

  • 首先,事件必须是冒泡的。某些事件不会冒泡。此外,低级处理程序不应使用 event.stopPropagation()
  • 其次,委托可能会增加 CPU 负载,因为容器级处理程序会对容器中任何地方的事件做出反应,无论我们是否感兴趣。但通常负载可以忽略不计,因此我们不考虑它。

任务

重要性:5

这里有一系列带有删除按钮 [x] 的消息。让按钮起作用。

像这样

P.S. 容器上应该只有一个事件侦听器,使用事件委托。

为任务打开沙盒。

重要性:5

创建一个树,单击时显示/隐藏节点子项

要求

  • 只有一个事件处理程序(使用委托)
  • 在节点标题外部(在空白处)单击不应执行任何操作。

为任务打开沙盒。

解决方案有两部分。

  1. 将每个树节点标题包装到 <span> 中。然后,我们可以在 :hover 上使用 CSS 对其进行样式化,并精确处理文本上的点击,因为 <span> 宽度与文本宽度完全相同(与没有它不同)。
  2. 将处理程序设置到 tree 根节点,并处理对该 <span> 标题的点击。

在沙盒中打开解决方案。

重要性:4

使表格可排序:单击 <th> 元素应按相应列对其进行排序。

每个 <th> 都在属性中具有类型,如下所示

<table id="grid">
  <thead>
    <tr>
      <th data-type="number">Age</th>
      <th data-type="string">Name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>5</td>
      <td>John</td>
    </tr>
    <tr>
      <td>10</td>
      <td>Ann</td>
    </tr>
    ...
  </tbody>
</table>

在上面的示例中,第一列有数字,第二列有字符串。排序函数应该根据类型进行排序。

只支持 "string""number" 类型。

实际示例

P.S. 表格可以很大,行数和列数不限。

为任务打开沙盒。

重要性:5

为工具提示行为创建 JS 代码。

当鼠标移到具有 data-tooltip 的元素上时,工具提示应出现在其上方,当鼠标离开时,工具提示应隐藏。

带注释的 HTML 示例

<button data-tooltip="the tooltip is longer than the element">Short button</button>
<button data-tooltip="HTML<br>tooltip">One more button</button>

应按如下方式工作

在此任务中,我们假设所有具有 data-tooltip 的元素内部只有文本。没有嵌套标签(目前)。

详细信息

  • 元素和工具提示之间的距离应为 5px
  • 如果可能,工具提示应相对于元素居中。
  • 工具提示不应越过窗口边缘。通常它应位于元素上方,但如果元素位于页面顶部且没有工具提示的空间,则应位于元素下方。
  • 工具提示内容在 data-tooltip 属性中给出。它可以是任意 HTML。

您需要在此处使用两个事件

  • 当指针移到元素上方时,触发 mouseover
  • 当指针离开元素时,触发 mouseout

请使用事件委托:在 document 上设置两个处理程序以跟踪具有 data-tooltip 的元素的所有“覆盖”和“退出”,并从此处管理工具提示。

在实现该行为后,即使不熟悉 JavaScript 的人也可以添加带注释的元素。

P.S. 一次只能显示一个工具提示。

为任务打开沙盒。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行,请将其包装在 <pre> 标签中,对于超过 10 行,请使用沙盒 (plnkrjsbincodepen…)