DOM事件和事件委托

133 阅读4分钟

一、DOM事件模型

先看一段代码

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

给3个div分别添加事件监听fnYe/fnBa/fnEr。有2个问题:

1. 提问1:点击了谁?

点击文字,算不算点击儿子?

点击文字,算不算点击爸爸?

点击文字,算不算点击爷爷?

答案:都算

2. 提问2:调用循序

点击文字,最先调用fnYe/fnBa/fnEr中的那一个函数?

答案:都行。IE5调用顺序为fnEr->fnBa->fnYe, 网景调用顺序为fnYe->fnBa->fnEr。

W3C在2002年发布了标准, 规定浏览器同时支持两种调用顺序.首先按爷爷->爸爸->儿子顺序看有没有函数监听, 然后按儿子->爸爸->爷爷顺序看有没有函数监听.

专业术语是DOM事件模型的事件捕获事件冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。

二、事件传播机制

事件的传播机制: 捕获节点 - 目标阶段 - 冒泡阶段

window-> document -> HTML ->body -> outer -> inner -> center

  • 当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到window
  • 当然其传播的是事件,绑定的执行函数并不会传播,如果父级没有绑定事件函数,就算传递了事件,也不会有什么表现,但事件确实传递了

因此DOM事件模型分为3个阶段:

  1. 捕获阶段:事件从window对象自上而下向目标节点传播的阶段
  2. 目标阶段:真正的目标节点正在处理事件的阶段
  3. 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段

图片1.png

三、捕获和冒泡

  • 由外向内找监听函数, 叫事件捕获.
  • 由内向外找监听函数, 叫事件冒泡.

四、绑定事件API

EventTarget.addEventListener('click', fn, bool)

  1. 如果 bool 不传或为 false
  • 就让 fn 走冒泡,即当浏览器在冒泡阶段发现 EventTargetfn 监听函数,就会调用 fn,并提供事件信息
  1. 如果 booltrue
  • 就让 fn 走捕获,即当浏览器在捕获阶段发现 EventTargetfn 监听函数,就会调用 fn ,并提供事件信息

五、W3C事件模型

  • 先捕获,再冒泡
  • 注意 e 对象被传给所有监听函数
  • 事件结束后,e 对象就不存在了

六、e.target 和 e.currentTarget 区别

区别

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

举例

  <div>
    <span>文字</span>
  </div>
  • 用户点击文字
  • e.target 就是 span
  • e.currentTarget 就是 div

七、取消冒泡

捕获不可取消,但冒泡可以

  • Event.stopPropagation() 可中断冒泡
  • 一般用于封装某些独立组件

八、不可阻止默认动作

有些事件不可阻止默认动作

  • MDN 搜索 scroll event,看到 Bubbles 和 Cancelable
  • Bubbles 的意思是该事件是否冒泡,所有冒泡都可取消
  • Cancelable 的意思是开发者是否可以阻止默认事件
  • Cancelable 与冒泡无关
  • 推荐看 MDN 英文版,中文版内容不全

图片2.png

九、如何阻止滚动

scroll 事件 不可阻止默认动作

  • 阻止 scroll 默认动作没用,因先有滚动才有滚动事件
  • 要阻止滚动,可阻止 wheeltouchstart 的默认动作
  • 注意你需要找准滚动条所在的元素,示例如下:
x.addEventListener('wheel',(e)=>{
    e.preventDefault() // 禁用滚动条
})
x.addEventListener('touchstart',(e)=>{
    e.preventDefault() // 针对移动端禁用触摸
})
  • 但是滚动条还能用,可用 CSS 让滚动条 width: 0
::-webkit- scrollbar {
whdth:0 ! important
}

十、自定义事件

创建一个自定义事件

// html 如下
<div id=div1>
    <button id=button1>
    点击触发 x 事件     
    </button>
</div>
// js 如下
button1.addEventListener('click', ()=>{
  const event = new CustomEvent("x", {"detail":{name:'x', age: 18}})
  button1.dispatchEvent(event)
})

button1.addEventListener('x', (e)=>{
  console.log('x')
  console.log(e)
})

十一、事件委托

事件代理(Event Delegation),又称之为事件委托。是JavaScript中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定在子元素的响应事件(click、keydown......)委托给父元素,让父元素担当事件监听的职务。

场景一

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

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

// html代码
<div id = 'div1'>
        <span>span</span>
        <button data-id='1'>click1</button>
        <button data-id='2'>click2</button>
        <button data-id='3'>click3</button>
        <button data-id='4'>click4</button>
        <button data-id='5'>click5</button>
        <button data-id='6'>click6</button>
        <button data-id='7'>click7</button>
        <button data-id='8'>click8</button>
        <button data-id='9'>click9</button>
        <button data-id='10'>click10</button>
      </div>
 // js代码
div1.addEventListener('click',(e)=>{
    const t = e.target
    if(t.tagName.toLowerCase()==='button'){
      console.log('button 被点击了')
      console.log('button data-id是'+t.dataset.id)
  }
  })//toLowerCase小写

场景二

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

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

// html代码
<div id = 'div1'>
      
      </div>
// js代码
setTimeout(()=>{
const button = document.createElement('button')
button.textContent = 'click1'
div1.appendChild(button)
},1000)

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

优点

  • 省监听数(内存):可以大量节省内存占用,减少事件注册,比如在ul上代理所有li的click事件
  • 可以监听动态元素:可以实现当新增子对象时无需再次对其绑定(动态绑定事件)