一次性了解DOM事件模型(机制)和事件委托

192

DOM 事件模型(机制)

DOM 的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequest、AudioNode、AudioContext)也部署了这个接口。

该接口主要提供三个实例方法。

  • addEventListener:绑定事件的监听函数
  • removeEventListener:移除事件的监听函数
  • dispatchEvent:触发事件

事件模型

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

(这种三阶段的传播模型,使得同一个事件会在多个节点上触发。)

  1. 由外向内找监听函数就是事件捕获
  2. 在目标节点触发事件
  3. 由内而外找监听函数就是事件冒泡

DOM事件传播的三个阶段:捕获阶段,目标阶段,冒泡阶段

举个例子:

<div class="grandfather">
  <div class="father">
    <div class="son">
        文字
    </div>
  </div>
</div>

给三个div分别添加事件的监听 fnYe / fnBa / fnEr,那么请问,点击文字,优先执行哪个函数呢?

这个涉及到一个小历史:

IE5认为先调用fnEr,网景认为先调用fnYe,最后遇到了W3C

2002年,w3c发布标准
文档名为DOM Level 2 Events Specification
规定浏览器应该同时支持两种调用顺序
首先按照grandfather->father->son
然后按照son->father->grandfather
那岂不是 fnYe / fnBa / fnEr 都调用两次,非也!
开发者可以自己决定把fnYe放在捕捉阶段还是放在冒泡阶段

W3C: baba.addEventListener('click',fn,bool)

如果bool不传或为false

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

如果bool为true

就让fn走捕获,即当浏览器在捕获阶段发现baba有fn监听函数,就会调用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;//把元素的变为透明
}

JavaScript:

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)
level1.addEventListener('click',fa)
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)

这里说一下几个知识点

currentTarget 事件属性

定义和用法

currentTarget 事件属性返回其监听器触发事件的节点,即当前处理该事件的元素、文档或窗口。
在捕获和起泡阶段,该属性是非常有用的,因为在这两个节点,它不同于 target 属性。更多细节可以==>MDNcurrentTarget

DOM事件模型

先捕获(先爷爷=>儿子)再冒泡(再儿子=>爷爷)

注意e对象被传给所有的监听函数

事件结束后,e对象就不存在了

target v.s. currentTarget的区别

区别:

e.target - 用户操作的元素
e.currentTarget-程序员监听的元素
this是e.currentTarget,我个人不推荐使用它

举例:

div>span{文字},用户点击文字
e.target就是span
e.currentTarget就是div

一个特例!!!

背景:

只有一个div被监听(不考虑父子同时被监听)

fn分别再捕获阶段和冒泡阶段监听click事件

用户点击的元素就是开发者监听的

代码:

div.addEventListenter('click',f1)
div.addEventListenter('click',f2,true)

请问,f1先执行还是f2先执行?

如果把两个调换位置?

总结:谁先监听谁先执行。

level7.addEventListener('click',()=>{
      console.log(1)
})//冒泡

level7.addEventListener('click',()=>{
      console.log(2)
},true)//捕获

//打印结果是 1 2

e.stopPropagation():取消冒泡

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

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

不可以取消冒泡

具体哪些可以查看MDN,这里举个例子:比如scroll

那么如果我就是想阻止它呢?可==>

x.addEventListener('wheel',(e)=>{
  e.preventDefault()
})//取消滚轮效果
x.addEventListener('touchstart',(e)=>{
  e.preventDefault()
})//取消触摸效果

但是有时候你会发现,旁边那个滚动条拖动还是可以啊,没事,直接给它干掉

::-webkit-scrollbar{
  width: 0 !important}

事件委托:

我委托一个元素帮我监听我本该监听的东西,比如onclick

场景1:

要给100个按钮添加点击事件,咋办?

答:监听这个100个按钮的祖先,等冒泡的时候判断target是不是这100个按钮中的一个

代码:

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

场景2:

你要监听目前不存在的元素的点击事件?

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

优点:省监听数(内存),可以动态监听元素

代码:

setTimeout(()=>{
  const button = document.createElement('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')
  }
})

封装一个事件委托

只要实行一个函数就可以实现事件委托

要求:

写出这样一个函数on('click','#testDiv','li',fn)

当用户点击#testDiv里面的li元素时,调用fn函数

要求用到事件委托

答:判断target是否匹配'li',即给一个元素加一个监听,看当前的target是否满足监听函数中的条件,如果满足调用,不满足放过。

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

on('click','#div1','button',()=>{//'#div'是选择器不是元素
  console.log('button 被点击啦')
})
function on(eventType,element,selector,fn){
  if(!(element instanceof Element)){
       element = document.querySelector(element)
     }
  element.addEventListener(eventType,(e)=>{
  const t= e.target//被点击的元素是span不是button啦
  if(t.matches(selector)){//matches用来判断一个元素是否匹配一个选择器,selector是不是一个选择器
span不匹配button
    fn(e)
   }
})
}