简述DOM事件机制和事件委托

574 阅读4分钟

1.事件机制

事件是在编程时系统内发生的动作或者发生的事情,系统会在事件出现的时候触发某种信号并且提供一个自动加载某种动作的机制。每个事件都有事件处理器(有时也叫事件监听器),也就是触发事件时运行的代码块。严格来说事件监听器监听事件是否发生,然后事件处理器对事件做出反应。

2.DOM事件流

事件流(Event Flow)指的是网页元素接收事件的顺序。事件流可以分为两种机制:

  • 事件捕获(Event Capturing)
  • 事件冒泡(Event Bubbling) 当一个事件发生后,会在子元素和父元素之间传播。这种传播分为三个阶段:
  1. 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
  2. 目标阶段:真正的目标节点正在处理事件的阶段;
  3. 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

事件捕获指的是从最外层元素(祖先元素)触发事件响应函数,逐级往下,直到目标元素。示意图如下:

图片.png

而事件冒泡的流程刚好是事件捕获的逆过程。当事件发生时,先触发目标元素(最直接元素),然后触发其父元素的事件响应函数,并逐级上溯到祖先元素。

2002年,W3C发布标准,规定浏览器应该同时支持两种调用顺序,先捕获后冒泡。示意图如下:

图片.png

<td>的click事件发生时,会先走红色的「capture phase」:

 1.Document
 2.<html>
 3.<body>
 4.<table>
 5.<tbody>
 6.<tr>
 7.<td> (实际被点击的元素)

由上而下依序触发它们的click事件。

然后到达「Target phase」后再继续执行绿色的「bubble phase」,反方向由<td>一路往上传至Document,整个事件流到此结束。

3. 阻止事件传播

在嵌套的元素中,并且每个元素都有事件处理程序时,当单击内部元素,所有处理程序都将同时执行,因为事件会出现在DOM树中。为了防止这种情况,可以使用e.stopPropagation()中断冒泡,浏览器不再向上走。

有些事件具有与之关联的默认操作。例如点击一个链接浏览器带你到链接的目标,点击一个表单提交按钮浏览器提交表单等等。可以使用事件对象的preventDefault()方法来防止此类默认操作。但是,阻止默认操作并不会停止事件传播,事件像往常一样继续传播到DOM树。

但有些事件不能阻止默认动作。比如MDN搜索scroll event,看到BubblesCancelable,Bubbles的意思是该事件是否冒泡,所有冒泡都可取消。Cancelable的意思是开发者是否可以阻止默认事件。

image.png

4.事件委托

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。

优点

  1. 减少内存消耗,提高性能 假设要给100个按钮添加点击事件,如果给每个按钮一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件委托,我们只需要给这100个按钮的父容器绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。 代码示例如下:
<div id="div1">
  <button>click 1</button>
  <button>click 2</button>
  <button>click 3</button>
  <button>click 4</button>
  <button>click 5</button>
</div>

<script>
div1.addEventListener('click', (e)=> { 
  //把目标元素赋给t
  const t = e.target
  // 判断是否匹配目标元素
  if (t.tagName.toLowerCase() === 'button') {
    console.log('button内容是:' + t.textContent);
  }
});
</script>
  1. 可以监听动态元素 假设要监听目前不存在的元素的点击事件,我们可以通过监听该元素的父容器,等点击的时候看看是不是我想要监听的元素即可。代码示例如下:
<div id="div1">
</div>
<script>
setTimeout(()=>{
  //div1里面添加一个button
  const button = document.creatElement('button')
  button.textContent = 'click 1'
  div1.appendChild(button)
},1000) 

div1.addEventListener('click',(e)=>{
    const t = e.target
    if(t.tagName.toLowerCase() === 'button'){
      console.log('button被click')
    }
});  
</script>

封装事件委托

<div id="div1">
</div>
<script>
setTimeout(()=>{
  const button = document.creatElement('button')
  button.textContent = 'click 1'
  div1.appendChild(button)
},1000) 

 on('click','#div1','button',()=>{
 console.log('button被点击了')
 })  
  
 function on(eventType, element, selector, fn){
   //判断如果element不是元素
   if(!(element instanceof Element)){
    element = document.querySelector(element)
   }
   element.addEventListener(eventType,(e)=>{
     const t = e.target
     //matches判断一个元素是否满足一个选择器
     if(t.matches(selector)){
        fn(e)
     }
  })
 }
</script>

本文参考:

JavaScript事件三部曲之事件机制的原理

JavaScript漫谈之DOM事件机制

DOM事件机制

DOM 事件机制&事件委托