事件委托

事件委托其实不是什么特别晦涩难懂的知识,其背后的知识点是关于事件的机制 (事件冒泡)。

我们知道,为单独一个元素注册事件监听器可以使用 on<event>、 元素行内属性和 addEventListener() 这些方法轻松实现,但是,当这些元素的数量递增时:

<ul id="parent">
  <li id="child1">child</li>
  <li id="child2">child</li>
  <li id="child3">child</li>
  <li id="child4">child</li>
  <li id="child5">child</li>
  ...
</ul>

再像以前那样为每一个子元素绑定一个监听器明显是个坏主意。

在说解决方案 (就是事件委托😂) 前,首先要说一下关于事件触发循序的两个模型 (model):事件捕获 (Event capturing) 和事件冒泡 (Event bubbling)。

Netscape vs Microsoft !!

  • Netscape said that the event on element1 takes place first. This is called event capturing.

  • Microsoft maintained that the event on element2 takes precedence. This is called event bubbling.

因此, W3C 机智的采取中立模型 (W3C model),即保留两个模型。将事件流分为三个阶段,如下图:

eventflow.png

在捕获阶段:

  • 浏览器检查元素的最外层祖先<html> (若 document 或 全局对象也注册了处理程序,则它们先) ,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。

  • 然后,它移动到<html>中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。

在目标阶段:

  • 事件对象到达事件对象的目标(元素),若冒泡阶段不冒泡(即捕获完毕),事件流将会在这个阶段停止。

在冒泡阶段,恰恰相反:

  • 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它

  • 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素(若 document 或 全局对象也注册了处理程序,则它们最后)。

由图可以知道,在 W3C模型 中,事件捕获阶段发生时间是比事件冒泡要早的。而且 W3C 推崇使用的是 addEventListener(type, listener, useCapture) 这个方法来实现两者兼存的,其中第三个参数接收一个布尔值,当该值为 true 时,该元素事件触发执行事件捕获阶段。

看以下代码:

<body onclick="console.log('body!')">  <!-- 这种方式无法使用事件捕获 -->
  <div id="parent">
  <div id = "child">
  </div>
</div>
</body>

添加监听器:

const parent = document.getElementById("parent");
const child = document.getElementById("child");
parent.addEventListener("click", () => {
  console.log("parent!");
},true); // 开启捕获阶段
child.addEventListener("click", () => {
  console.log("child!");
},false); // 关闭捕获阶段(默认)

//当点击child元素时,输出以下
//parent!
//child!
//body!

可以使用 event.stopPropagation() 这个方法阻止事件的捕获或者冒泡。

上面的 parent 元素改为:

parent.addEventListener("click", e => {
  console.log("parent!");
  console.log("stopPropagation!");
  e.stopPropagation();
});

//此时输出为:
//parent!
//stopPropagation!

所以我们直接回到事件委托,有了上面的知识后,事件委托其实就很容易理解了;事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件

回接上面代码,我们直接给父 ul 注册监听器就可以解决问题:

const parent = document.getElmentById("parent");

parent.addEventListener("click", e => {
  let target = e.target; // 目标阶段,获取事件对象的事件目标
  switch(target.id){
    case "child1":
      console.log("我是老大!");
      break;
    case "child2":
      console.log("我是老二!");
      break;
    case "child3":
      console.log("我是老三!");
        break;
    // ...
  }
}, false);

总之,这本身是没有什么特别的东西,主要是对事件捕获和冒泡的一种复习。后续再扯一下自定义事件和冒泡的实现。😂

Reference: