基本概念

事件委托,通俗地来讲,就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素;
一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
举个例子,有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。
这里其实还有2层意思的:
第一,现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的;
第二,新员工也是可以被前台MM代为签收的,即程序中新添加的dom节点也是有事件的。

原理

事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件,举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。
假设div1有100个button,实现事件委托的代码如下:

  1. div1.addEventListener('click', (e) => {
  2. const t = e.target
  3. if (t.tagName.toLowerCase() === 'button') {
  4. console.log('button被点击了')
  5. }
  6. })

那么如何监听目前不存在的button呢,上面的代码同样可以完成这个任务,因为div1是动态判断button的点击的,所以即使是新加入一个button,它同样可以获取它的点击事件。
接下来,我们可以封装这个函数

  1. on('click', '#div1', 'button', () => {
  2. console.log('button被点击了')
  3. })
  4. function on(eventType, element, selector, fn) {
  5. if (!(element instanceof Element)) {
  6. element = document.querySelector(element)
  7. }
  8. element.addEventListener(eventType, (e) => {
  9. const t = e.target
  10. if (t.matches(selector)) {
  11. fn(e)
  12. }
  13. })
  14. }

优点

  • 减少事件注册,节省内存。比如,
    • 在table上代理所有td的click事件。
    • 在ul上代理所有li的click事件。
  • 简化了dom节点更新时,相应事件的更新。比如

    • 不用在新添加的li上绑定click事件。
    • 当删除某个li时,不用移解绑上面的click事件。

      缺点

  • 事件委托基于冒泡,对于不冒泡的事件不支持。

  • 层级过多,冒泡过程中,可能会被某层阻止掉。
  • 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在table上代理td,而不是在document上代理td。
  • 把所有事件都用代理就可能会出现事件误判。比如,在document中代理了所有button的click事件,另外的人在引用改js时,可能不知道,造成单击button触发了两个click事件。

    总结

    那什么样的事件可以用事件委托,什么样的事件不可以用呢?
    适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。
    值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
    不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。