事件委托

57 阅读5分钟

一、基本概念

要想了解事件委托,必先了解事件流

事件流即事件的发生顺序,阶段分为:

1. 捕获阶段:从根节点开始顺着目标节点构建一条事件路径,首先 window 会获捕获到事件,之后 documenthtmlbody 会捕获到,再之后就是在 body 中 DOM 元素一层一层的捕获到事件2. 目标阶段:到达目标节点,即元素本身3. 冒泡阶段:从目标节点顺着捕获阶段构建的路径返回去, 逐级向上,直至 window

如图 👇

1~5 是捕获阶段,5~6 是目标阶段,6~10 是冒泡阶段;

应用最多的是 冒泡阶段,比如事件委托

二、事件委托

事件委托又叫事件代理,利用了事件的冒泡机制,将节点上的事件委托给上层节点处理。

一个栗子:

有三个同事预计会在周一收到快递,为了签收快递,有两种办法:方法 1. 三个人在公司门口等快递方法 2. 等快递到了让前台妹子代为签收

第一种方法:缺点是每个人都要在门口等着,如果人数变为十个,二十个那公司也不会容忍那么多员工站在门口就为了等快递

第二种方法:前台妹子收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优点,那就是即使公司里来了新员工,前台妹子也可以在收到寄给新员工的快递后核实并代为签收。

8117c4d19b7d750efa1d0d210c233f05_75.jpg

第二种方法就是事件委托,翻译过来就是:1. 前台妹子可以代收同事们的快递,即能获取到现有的 DOM 节点的事件2. 新员工的快递也是可以被前台妹子代为签收,即也能获取到程序中新添加的 DOM 节点的事件

三、为什么要有事件委托

如果 DOM 需要有事件处理程序,那我们直接给它设事件处理程序就好了

如果是很多的 DOM 需要添加事件处理呢?比如我们有 5 个 li,每个 li 都有相同的 click 点击事件,那我们直接用 for 循环的方法,来遍历所有的 li,然后给它们添加事件,请看👇:

<ul id='ul'>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul> 
const ul = document.querySelector("ul");  
const liList = ul.getElementsByTagName('li');
for (let i = 0; i < liList.length; i++) {
  liList[i].addEventLister('click', (e) => {
    console.log(`点击了${e.target.innerHTML}, 索引为 ${i}`);
  });
}

弊端:

1. 与 DOM 的交互增多:添加到页面上的事件数量将直接关系到页面的整体运行性能,因为需要不断的与 DOM 节点进行交互,访问 dom 的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少 DOM 操作的原因;如果要用事件委托,就会将所有的操作放到 js 程序里面,与 DOM 的操作就只需要交互一次,这样就能大大的减少与 DOM 的交互次数,提高性能;

2. 内存占用大:每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了,比如上面的 5 个 li,就要占用 5 个内存空间,如果是 1000 个,10000 个呢,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了

3. 新增加的 DOM 元素无法获取事件

用事件委托可以完美规避上面三个弊端,那么问题来了,怎么用事件委托实现呢,请看👇

const ul = document.querySelector('ul');
ul.addEventLister("click", e => {
  const { target } = e;
  if (target.tagName.toLowerCase() === 'li') {
    const liList = this.querySelectorAll('li');
    const index = Array.prototype.indexOf.call(liList, target); // 因为 liList 不是真正的数组,是节点集合,对于非真正数组,可以通过 call 来改变 this 指向,调用 Array 原型链上的各种方法
    console.log(`点击了${target.innerHTML}, 索引为 ${index}`);
  }
});

四、面试官问

问题一:

问题描述:window.addEventListerner 监听的是什么阶段的事件 ?

答:要看第三个参数的值

1. 如果第三个参数为 true,则为捕获阶段

window.addEventLister('click',  () => {
    // todo
  }, true);

2. 如果第三个参数为 false,或者没传(默认值为 false),则为冒泡阶段

window.addEventLister('click', () => {
  // todo
});

问题二:

问题描述:页面上,有若干按钮,每个按钮都有自身的点击事件需求是,每个用户有个是否被封禁的属性 banned = true,封禁用户点击页面上任何按钮都不可响应原来的函数,而是直接 alert 出提示信息:'你被封禁了'

方法 1: 在每个 click 事件里加个判断

if (banned) {
  alert('你被封禁了');
  return;
}

缺点:1. 改动较多,冗余代码多,不优雅2. 新增加的 DOM 节点无此判断

方法 2: 如果是封禁用户,在整个页面上加一层透明的遮罩,并添加 click 事件,alert 出 '你被封禁了',这样点击的时候就只会点击到这层遮罩上。 缺点:需要额外添加页面元素

方法 3: 在最顶层监听捕获的阶段

window.addEventLister(
  "click",
  (e) => {
    if (banned) {
      e.stopProgagtion();
      alert('你被封禁了');
    }
  },
  true
);

五、事件委托的使用场景

1. 适合用事件委托的有:clickmousedownmouseupkeydownkeyupkeypress

2. 不适合用事件委托的有:focusblur 之类的,本身就没用冒泡的特性,所以就无法用事件委托了

值得注意的是,mouseovermouseout 虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。特别是 mousemove,每次都要计算它的位置,非常不好把控,建议不要用