入手
<div class="爷爷">
<div class="爸爸">
<div class="儿子">
文字
</div>
</div>
</div>
// 给三个div分别添加事件监听fnYe/fnBa/fnEr
点击了谁?
- 点击文字,算不算点击儿子?
- 点击文字,算不算点击爸爸?
- 点击文字,算不算点击爷爷?
答案:都算
调用顺序
- 点击文字,最先调用fnYe /fnBa /fnEr中的哪一个函数?
答案:都行。IE5认为先调用fnEr,网景认为先调用fnYe。
W3C
2002年, W3C发布标准
- 文档名为DOM Level 2 Events Specification。
- 规定浏览器应该同时支持两种调用顺序。
- 首先按 爷爷=>爸爸=>儿子 顺序看有没有函数监听。
- 然后按 儿子=>爸爸=>爷爷 顺序看有没有函数监听。
- 有监听函数就调用,并提供事件信息,没有就跳过。
术语
从外向内找监听函数,叫事件捕获。
从内向外找监听函数,叫事件冒泡。
那这样岂不是fnYe/fnBa/fnEr都调用两次?非也!开发者自己选择把fnYe放在捕获阶段还是冒泡阶段。
示意图
W3C 事件模型
addEventListener
各家的事件绑定API都是什么呢?
- IE5*:
xxx.attachEvent('onclick', fn)
// 冒泡 - 网景:
xxx.addEventListener('click',fn)
// 捕获
然后W3C就把两家综合起来:xxx.addEventListener('click', fn, bool)
如果bool不传或为falsy:就让fn走冒泡,即当浏览器在冒泡阶段发现xxx有fn监听函数,就会调用fn,并提供事件信息。
如果bool为true:就让fn走捕获,即当浏览器在捕获阶段发现xxx有fn监听函数,就会调用fn,并提供事件信息。
注意:浏览器两个阶段都要进行,只是通过bool值控制什么时候调用fn。
代码演示
jsbin.com/mifiyuhane/…
代码图解
如图所示:e对象是被传给所有监听函数,事件结束后,e对象就不存在了。
target vs currentTarget
区别:
- e.target 用户操作的元素
- e.currentTarget 程序员监听的元素
举例: - div > span{文字} 用户点击文字
- e.target 就是span
- e.currentTarget 就是div
取消冒泡
e.stopPropagation()
有的事件不能取消冒泡
- 可以在MDN上搜索,例如scroll event。
- Bubbles就是该事件是否冒泡。
- Cancelable就是开发者是否可以取消冒泡。
如何阻止滚动?
由上可知,scroll事件不可取消冒泡,那我们如何阻止滚动呢?
<div id=x>
<p>1</p>
...
<p>30</p>
</div>
- 阻止scroll默认动作没用,因为先有滚动才有滚动事件。
x.addEventListenet('scroll',(e)=>{
e.stopPropagation()
e.preventDefault()
})
我们取消冒泡和默认事件,都不能阻止滚动。
2. 要阻止滚动,可阻止wheel和touchstart的默认动作。
注意需要找准滚动条所在的元素
wheel是鼠标滚轮,touchstart针对手机滑动。
x.addEventListener('wheel',(e)=>{
e.preventDefault()})
x.addEventListener('otuchstart',(e)=>{
e.preventDefault()})
- 但是此时滚动条还显示,用鼠标可以拖动滚动滚动。
我们可以在css里写入下面的语句,就可将滚动条隐藏
::-webkit-scrollbar{
width:0 !important}
- CSS使用overflow:hidden也可以直接取消滚动条。但此时JS依然可以修改scrollTop。
自定义事件
浏览器自带事件100多种 事件参考 | MDN (mozilla.org)
那开发者如何在自带事件之外,自定义一个事件?
例如我们要自定义一个xxx的事件。<button id=btn>
点击时触发 xxx 的事件
btn.addEventListener('click', ()=>{
const event = new CustomEvent('xxx', {
detail:{name:'xxx', age:23},
bubbles: true,
cancelable: false
})
btn.dispatchEvent(event)
})
这样我们就自定义了一个事件。
事件委托
场景1:假设要给100个按钮添加点击事件,咋办?
监听这100个按钮的祖先,等冒泡的收判断target是不是这100个按钮中的一个。
//html
<div id=div1>
<button data-id="1">click 1</button>
...
<button>click 2</button>
</div>
// js
div1.addEventListener('click',(e)=>{
const t = e.target
if(t.tagName.toLowerCase() === 'button'){
console.log('button 被点击了')
console.log(t.textContent)
console.log(t.dataset.id)
}
})
场景2:怎么监听目前不存在的元素的点击事件?
监听祖先,等点击的时候看看是不是我想要监听的元素即可。
//html
<div id=div1></div>
// js
setTimeout(()=>{
const button=document.createElement('button')
button.textContent = 'click 1'
div1.apendChild(button)
}, 1000)
div1.addEventListener('click), (e)=>{
const t = e.target
if(t.tagName.toLowerCase() === 'button'{
console.log('button 被点击了')
}
})
优点:省监听数(内存)
可以监听动态元素
怎么封装一个事件委托呢?
// 调用
on('click', '#div1', 'button', ()=?{
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
if(t.matches(selector)){
fn(e)
}
})
}