DOM 事件模型或 DOM 事件机制

858 阅读4分钟

事件

通俗理解,事件是用户或者浏览器自己执行的某种动作,是文档或者浏览器发生的一些交互瞬间,比如点击(click)按钮等,这里的click就是事件的名称。JS与html之间的交互是通过事件实现的。

每个事件都有事件监听器(有时也叫事件监听器),也就是触发事件时运行的代码块。严格来说事件监听器监听事件是否发生,然后事件处理器对事件做出反应。

事件模型

一个事件发生后,会在子元素及父元素之间进行传播(propagation),这种传播分为三个阶段。

DOM事件传播的三个阶段:

  • 捕获阶段
  • 目标阶段
  • 冒泡阶段

事件捕获和事件冒泡

  • 事件捕获:由外向内找监听函数。
  • 事件冒泡:由内向外找监听函数。

例子:

<div class="grandfather">
  <div class="father">
    <div class="son"></div>
    hi
  </div>
</div>
//假如给三个div分别添加事件监听 fnYe / fnBa / fnEr

问题一:

  • 点击文字,算不算点击儿子?
  • 点击文字,算不算点击爸爸?
  • 点击文字,算不算点击爷爷? 答案:都算

问题二: 调用监听函数顺序是什么呢?

事件捕获:fnYe > fnBa > fnEr , 也就是从外到内去调用

事件冒泡:fnEr > fnBa > fnYe , 也就是从内到外去调用

W3C在2002年发布了标准, 文件名为DOM Level 2 Events Specification

规定浏览器同时支持两种调用顺序

首先按爷爷->爸爸->儿子顺序看有没有函数监听 , 先事件捕获

然后按儿子->爸爸->爷爷顺序看有没有函数监听 , 再事件冒泡

如图所示:

1.png

如何指定走捕获还是冒泡呢?

baba.attachEvent('onclick',fn)//冒泡
baba.addEventListener('click',fn)//捕获
baba.addEventListener('click',fn,bool)//w3c制定
  • 如果bool不传或为falsy
  • 就让fn走冒泡,即当浏览器在冒泡阶段发现 babafn 监听函数,就会调用fn,并提供事件信息。
  • 如果bool为true
  • 就让fn走捕获,即当浏览器在捕获阶段发现 babafn 监听函数,就会调用fn,并且提供事件信息。

代码示例:

HTML:

<div class="level1 x">
  <div class="level2 x">
    <div class="level3 x">
      <div class="level4 x">
        <div class="level5 x">
          <div class="level6 x">
            <div class="level7 x">
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

CSS:

* {
  box-sizing: border-box;
}
div[class^=level] {
  border: 1px solid;
  border-radius: 50%;
  display: inline-flex;
}
.level1 {
  padding: 10px;
  background: purple;
}
.level2 {
  padding: 10px;
  background: blue;
}
.level3 {
  padding: 10px;
  background: cyan;
}
.level4 {
  padding: 10px;
  background: green;
}
.level5 {
  padding: 10px;
  background: yellow;
}
.level6 {
  padding: 10px;
  background: orange;
}
.level7 {
  width: 50px;
  height: 50px;
  border: 1px solid;
  background: red;
  border-radius: 50%;
}
.x{
  background: transparent;
}

JS:

const level1 = document.querySelector('.level1')
const level2 = document.querySelector('.level2')
const level3 = document.querySelector('.level3')
const level4 = document.querySelector('.level4')
const level5 = document.querySelector('.level5')
const level6 = document.querySelector('.level6')
const level7 = document.querySelector('.level7')
let n = 1
 const fm = (e)=>{
  const t = e.currentTarget
  setTimeout(()=>{  
    t.classList.remove('x')
  },n*1000)
  n+=1
}
 const fa = (e)=>{
   const t =e.currentTarget
   setTimeout(()=>{
     t.classList.add('x')
   },n*1000)
   n+=1
 }
level1.addEventListener('click',fm,true) //true 走捕获
level1.addEventListener('click',fa) //默认为 false 走冒泡
// 先捕获后冒泡
level2.addEventListener('click',fm,true)
level2.addEventListener('click',fa)
level3.addEventListener('click',fm,true)
level3.addEventListener('click',fa)
level4.addEventListener('click',fm,true)
level4.addEventListener('click',fa)
level5.addEventListener('click',fm,true)
level5.addEventListener('click',fa)
level6.addEventListener('click',fm,true)
level6.addEventListener('click',fa)
level7.addEventListener('click',fm,true)
level7.addEventListener('click',fa)

一个特例

//只有一个 div 被监听(不需要考虑父子关系)
div.addEventListenter('click',f1) //冒泡
div.addEventListenter('click',f2,true) //捕获

f1 先执行还是 f2 先执行呢?

先捕获在冒泡, f2 先执行?

正确答案:f1 先执行。

当没有父子关系时,谁先监听谁就先执行。

target 与 currentTarget的区别

  • e.target 用户操作的元素
  • e.currentTarget 程序员监听的元素

例子:

<div>
  <span>文字</span>
</div>
  • e.target 就是 span ,用户操作的元素。
  • e.currentTarget 就是 div , 程序员监听的元素。

取消冒泡

e.stopPropagation() 可打断冒泡,浏览器不再向上走

一般用于封装某些独立组件

注意:捕获不可以取消但是冒泡可以(有些事件也不能够取消冒泡,例如: scroll 滚动条)

事件委托

事件委托的好处:

  1. 省内存(省监听数)
  2. 可以监听动态元素

例子一:

//有 100 个 button
<div id="div1">
  <span>span1</span>
   <button>click 1</button>
   <button>click 2</button>
   <button>click 3</button>
   <button>click 4</button>
   <button>click 5</button>
                 .
                 .
                 .
   <button>click 100</button>
</div>

如果有100个 button 怎么办呢,不可能创建 100 个监听器吧。

将监听委托给 div

我们只需要监听这个100个按钮的祖先,等冒泡的时候判断 e.target 是不是这100个按钮中的一个,节省内存。

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

例子二:

button 延迟1秒后才出现,我们如何监听呢?

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

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

监听祖先,等点击的时候看看是不是监听的元素即可,监听动态元素。

封装事件委托

setTimeout(()=>{
  const button = document.createElement('button')
  button.textContent='click'
  div1.appendChild(button)
},1000)

on('click','#div1','button',()=>{  //'#div'是选择器不是元素
  console.log('button 被点击啦')
})

// 声明 on 函数
function on(eventType,element,selector,fn){
//判断 element 类型是不是元素
  if(!(element instanceof Element)){
       // 不是则找到指定的元素并赋值给 element
       element = document.querySelector(element)
     }
  element.addEventListener(eventType,(e)=>{
  const t= e.target
  if(t.matches(selector)){ 
    fn(e)
   }
})
}