DOM事件和事件委托

342 阅读4分钟

一. 引入

<div class=爷爷>
  <div class=爸爸>
    <div class=儿子>
      文字
    </div>
  </div>
<div>

上面代码是一个点击事件的HTML代码,层级关系为 .爷爷 > .爸爸 > .儿子,现给三个 div 分别添加事件监听fnYe / fnBa / fnEr

提问1(点击了谁):点击文字,算不算点击儿子?算不算点击爸爸?算不算点击爷爷?

答:都算。

提问2(调用顺序):点击文字,最先调用fnYe / fnBa / fnEr中的哪一个函数?

答:都行。

二. DOM事件模型

1. 事件捕获和事件冒泡

当一个事件发生在具有父元素的元素上时,浏览器会运行两个不同的阶段 - 捕获阶段和冒泡阶段。

  • 事件捕获:从外向内找监听函数(爷爷=>爸爸=>儿子)
  • 事件冒泡:从内向外找监听函数(儿子=>爸爸=>爷爷)

捕获:当用户点击按钮,浏览器会从 window 从上往下遍历至用户点击的按钮,逐个触发事件处理函数。

冒泡:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。

2. W3C 事件模型/事件机制

对于引入描述的点击事件的调用顺序,IE5 认为先调用fnEr,网景认为先调用fnYe,后来W3C 发布标准,规定浏览器应该同时支持两种调用顺序。

首先按爷爷=>爸爸=>儿子(先捕获)顺序看有没有函数监听,然后按儿子=>爸爸=>爷爷(再冒泡)顺序看有没有函数监听。有监听函数就调用,并提供事件信息,没有就跳过。

对每个事件先捕获再冒泡,这就是 W3C 事件模型/事件机制。

3. addEventListener

关于点击事件例子的疑问:那岂不是 fnYe / fnBa / fnEr 都调用两次?

答:非也!开发者可以使用addEventListener()自己选择把函数监听放在捕获阶段还是放在冒泡阶段。addEventListener是一个事件绑定 API,其语法如下:

target.addEventListener('click', fn, bool)
  • 如果 bool 不传或为 falsy 值

fn 走冒泡,即当浏览器在冒泡阶段发现 target 有 fn 监听函数,就会调用 fn,并提供事件信息。

  • 如果 bool 为 true

fn 走捕获,即当浏览器在捕获阶段发现 target 有 fn 监听函数,就会调用 fn,并提供事件信息。

4. target v.s. currentTarget

区别

  • e.target - 用户操作的元素
  • e.currentTarget - 开发者监听的元素
  • this是e.currentTarget,但是不推荐使用

举例

div > span{文字},用户点击文字

  • e.target 就是 span
  • e.currentTarget 就是 div

5. 只有一个 div 被监听

只有一个 div 被监听时(不考虑父子同时被监听),fn 分别在捕获阶段和冒泡阶段监听 click 事件,此时用户点击的元素就是开发者监听的元素。

div.addEventListener('click', f1)  //冒泡
div.addEventListener('click', f2, true)  //捕获

提问:f1先执行还是f2先执行?如果把两行调换位置后,哪个先执行?

错误答案:f2先执行

正确答案:谁先监听谁先执行!

6. 取消冒泡——捕获不可取消,但冒泡可以

在现代浏览器中,默认情况下,所有事件处理程序都在冒泡阶段进行注册。

可以用e.stopPropagation()来中断冒泡,使浏览器不再向上走,一般用于封装某些独立的组件。

7. 阻止默认动作

使用e.preventDefault()来阻止默认动作,所有冒泡皆可取消,但是默认动作有的可以取消,有的不可取消,如 scroll 事件不可阻止默认动作。

三. 事件委托

事件委托就是,如果想要在大量子元素中单击任何一个都可以运行一段代码,那么可以将事件监听器设置在其祖先元素(如父元素、爷爷元素)上,并让子节点上发生的事件冒泡到祖先元素上,而不是每个子节点单独设置事件监听器。

事件委托的好处是:节约监听数量;可以监听动态生成的元素

举例1:要给100个按钮添加点击事件,可以监听这100个按钮的父节点,等冒泡的时候判断 target 是不是这100个按钮中的一个。

div1.addEventListener('click', (e) => {
  const t = e.target
  if (t.tagName.toLowerCase() === 'button') {
    console.log('button被点击了,内容是' + t.textContent)
  }
})

举例2:要监听目前不存在的元素的点击事件,可以监听父节点,等点击的时候看看是不是想要监听的元素。

//HTML
<div id="div1"></div>

//JS
setTimeout(() => {
  const button = document.createElement('button')
  button.textContent = 'click 1'
  div1.appendChild(button)
}, 1000)  //一秒后创建 button

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

封装事件委托

写出这样一个函数on('click', '#testDiv', 'li', fn),当用户点击#testDiv里的li元素时,调用fn函数,要求用到事件委托

function on(eventType, element, selector, fn){
  if(!(element instanceof Element)){
    element = document.querySelector(element)
  }
  element.addEventListener(eventType, (e) => {
    const t = e.target
    if(t.matches(selector)){  //判断target是否匹配'li'
      fn(e)
    }
  })
}

on('click', '#div1', 'button', ()=>{
  console.log('button 被点击了')
})

matches(selector) 用于判断用户操作的元素是否与selector匹配。

学习链接

MDN | 事件参考