一、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个阶段:
- 捕获阶段:事件从window对象自上而下向目标节点传播的阶段
- 目标阶段:真正的目标节点正在处理事件的阶段
- 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段
三、捕获和冒泡
- 由外向内找监听函数, 叫事件捕获.
- 由内向外找监听函数, 叫事件冒泡.
四、绑定事件API
EventTarget.addEventListener('click', fn, bool)
- 如果
bool不传或为false
- 就让 fn 走冒泡,即当浏览器在冒泡阶段发现
EventTarget有fn监听函数,就会调用fn,并提供事件信息
- 如果
bool为true
- 就让 fn 走捕获,即当浏览器在捕获阶段发现
EventTarget有fn监听函数,就会调用fn,并提供事件信息
五、W3C事件模型
- 先捕获,再冒泡
- 注意 e 对象被传给所有监听函数
- 事件结束后,e 对象就不存在了
六、e.target 和 e.currentTarget 区别
区别
- e.target - 用户操作的元素
- e.currentTarget - 程序员监听的元素
- this 是 e.currentTarget,我个人不推荐使用它
举例
<div>
<span>文字</span>
</div>
- 用户点击文字
e.target就是spane.currentTarget就是div
七、取消冒泡
捕获不可取消,但冒泡可以
Event.stopPropagation()可中断冒泡- 一般用于封装某些独立组件
八、不可阻止默认动作
有些事件不可阻止默认动作
- MDN 搜索 scroll event,看到 Bubbles 和 Cancelable
- Bubbles 的意思是该事件是否冒泡,所有冒泡都可取消
- Cancelable 的意思是开发者是否可以阻止默认事件
- Cancelable 与冒泡无关
- 推荐看 MDN 英文版,中文版内容不全
九、如何阻止滚动
scroll 事件 不可阻止默认动作
- 阻止
scroll默认动作没用,因先有滚动才有滚动事件 - 要阻止滚动,可阻止
wheel和touchstart的默认动作 - 注意你需要找准滚动条所在的元素,示例如下:
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事件
- 可以监听动态元素:可以实现当新增子对象时无需再次对其绑定(动态绑定事件)