前言
捕获和冒泡允许我们实现最强大的事件处理模式之一,即 事件委托 模式。
这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。
在处理程序中,我们获取 event.target 以查看事件实际发生的位置并进行处理。
事件委托简介
| Bagua Chart: Direction, Element, Color, Meaning | ||
|---|---|---|
| Northwest
         Metal Silver Elders  | 
      North
         Water Blue Change  | 
      Northeast
         Earth Yellow Direction  | 
    
| West
         Metal Gold Youth  | 
      Center
         All Purple Harmony  | 
      East
         Wood Blue Future  | 
    
| Southwest
         Earth Brown Tranquility  | 
      South
         Fire Orange Fame  | 
      Southeast
         Wood Green Romance  | 
    
该表格有 9 个单元格(cell),但可以有 99 个或 9999 个单元格,这都不重要。
我们的任务是在点击时高亮显示被点击的单元格 <td>。
与其为每个 <td>(可能有很多)分配一个 onclick 处理程序 —— 我们可以在<table>元素上设置一个“捕获所有”的处理程序。
它将使用 event.target 来获取点击的元素并高亮显示它。
代码如下:
let selectedTd;
table.onclick = function(event) {
  let target = event.target; // 在哪里点击的?
  if (target.tagName != 'TD') return; // 不在 TD 上?那么我们就不会在意
  highlight(target); // 高亮显示它
};
function highlight(td) {
  if (selectedTd) { // 移除现有的高亮显示,如果有的话
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // 高亮显示新的 td
}
此代码不会关心在表格中有多少个单元格。我们可以随时动态添加/移除 <td>,高亮显示仍然有效。
尽管如此,但还是存在缺陷。

点击可能不是发生在 <td> 上,而是发生在其内部。
在我们的例子中,如果我们看一下 HTML 内部,我们可以看到 <td> 内还有嵌套的标签,例如 <strong>:
在处理程序 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)
};
解释:
elem.closest(selector) 方法返回与 selector 匹配的最近的祖先。在我们的例子中,我们从源元素开始向上寻找
<td>。如果 event.target 不在任何
<td>中,那么调用将立即返回,因为这里没有什么事儿可做。对于嵌套的表格,event.target 可能是一个
<td>,但位于当前表格之外。因此我们需要检查它是否是我们的表格中的<td>。如果是的话,就高亮显示它。
委托示例:标记中的行为
事件委托还有其他用途。(译注:本节标题中的“标记中的行为”即 action in markup)
例如,我们想要编写一个有“保存”、“加载”和“搜索”等按钮的菜单。并且,这里有一个具有 save、load 和 search 等方法的对象。如何匹配它们?
第一个想法可能是为每个按钮分配一个单独的处理程序。但是有一个更优雅的解决方案。 我们可以为整个菜单添加一个处理程序,并为具有方法调用的按钮添加 data-action 特性(attribute):
<button data-action="save">Click to Save</button>
处理程序读取特性(attribute)并执行该方法。工作示例如下:
<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] 将不是我们所需要的。
那么,这里的委托给我们带来了什么好处?
- 我们不需要编写代码来为每个按钮分配一个处理程序。只需要创建一个方法并将其放入标记(markup)中即可。
 - HTML 结构非常灵活,我们可以随时添加/移除按钮。
 
我们也可以使用 .action-save,.action-load 类,但 data-action 特性(attribute)在语义上更好。我们也可以在 CSS 规则中使用它。
“行为”模式
我们还可以使用事件委托将“行为(behavior)”以 声明方式 添加到具有特殊特性(attribute)和类的元素中。
行为模式分为两个部分:
- 我们将自定义特性添加到描述其行为的元素。
 - 用文档范围级的处理程序追踪事件,如果事件发生在具有特定特性的元素上 —— 则执行行为(action)。
 
行为:计数器
例如,这里的特性 data-counter 给按钮添加了一个“点击增加”的行为。
<script>
  document.addEventListener('click', function(event) {
    if (event.target.dataset.counter != undefined) { // 如果这个特性存在...
      event.target.value++;
    }
  });
</script>
Counter: 
One more counter: 
总结
事件委托真的很酷!这是 DOM 事件最有用的模式之一。
它通常用于为许多相似的元素添加相同的处理,但不仅限于此。
算法
- 在容器(container)上放一个处理程序。
 - 在处理程序中 —— 检查源元素 event.target。
 - 如果事件发生在我们感兴趣的元素内,那么处理该事件。
 
好处
- 简化初始化并节省内存:无需添加许多处理程序。
 - 更少的代码:添加或移除元素时,无需添加/移除处理程序。
 - DOM 修改 :我们可以使用 innerHTML 等,来批量添加/移除元素。
 
事件委托也有其局限性
首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()。
其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。