前言
如下代码
<div class="爷爷">
<div class="爸爸">
<div class="儿子">
文字
</div>
</div>
</div>
我们给三个div分别添加事件监听fnYeye/fnBaba/fnErzi
提问1:
- 点击文字,算不算点击儿子?
- 点击文字,算不算点击爸爸?
- 点击文字,算不算点击爷爷? 答案:都算
提问2:
- 点击文字,最先调用fnYeye还是fnErzi?
答案:都行
因为在之前,IE5认为应该先调用fnErzi,而网景认为应该先调用fnYeye
直到2002年,W3C发布了标准:
- 规定浏览器应该同时支持两种调用顺序
- 首先按爷爷->爸爸->儿子顺序看有没有函数监听
- 然后按儿子->爸爸->爷爷顺序看有没有函数监听
- 有监听函数就调用,并提供事件信息,没有就跳过
我们把这两种不同顺序的监听形式添加一个术语:
- 从外向内找监听函数,叫作事件捕获
- 从内向外找监听函数,叫作事件冒泡
DOM事件机制
DOM事件模型分为捕获和冒泡,一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:
- 捕获阶段:事件从window对象自上而下向目标节点传播的阶段
- 目标阶段:真正的目标节点正在处理事件的阶段
- 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段
如图,我们可以很直观的理解
addEventListener
事件绑定API
div.addEventListener('click,fn,bool')- 第一个参数是事件类型,第二个参数是一个函数,第三个参数是bool值
- 浏览器自带100多种事件类型->事件参考_MDN,当然除此之外,用户也能自定义事件
如果bool不传或者为falsy
就让fn走冒泡,即当浏览器在冒泡阶段发现div有fn监听函数,会调用fn,并提供事件信息
举例:我们将圆环这样嵌套
<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>
level7填充红色,level1填充紫色
冒泡示例
当我们点击最中间的圆环时,它的颜色是自内向外填充的,点击任意圆环也都是自内向外
如果bool为true
就让fn走捕获,即当浏览器在捕获阶段发现div有监听函数,就调用fn,并提供事件信息
捕获示例
当我们点击最中间的圆环时,它的颜色是自外向内填充的,点击任意圆环也都是自外向内
所以如图所示,通过bool,就能决定把fn放在哪一边
当然,也可以两边都执行:
代码图解:
注:e并非真的消亡,但这样理解就行,无需深究
target 与 currentTarget
区别
e.target用户操作的元素e.currentTarget程序员监听的元素 举例
<div>
<span>文字</span>
</div>
假设我们监听的是div,而用户实际点击了'文字',则
e.target就是spane.currentTarget就是div
一个特例
按照上述来说,事件都是先捕获后监听的,
但如果只有一个div被监听(不考虑父子同时被监听),fn分别再捕获阶段和冒泡阶段对事件进行监听,
那么用户点击的元素就是开发者监听的元素
举例
div.addEventListener('click,f1')
div.addEventListener('click,f2,true')
这里是f1先执行还是f2先执行?
答案是f2先执行,因为在这个情况下,谁先监听谁先执行
取消冒泡
注:捕获不可取消,但冒泡可以
e.stopPropagation()中断冒泡,浏览器不再往上走
取消冒泡示例
如图,在第四个圆环处,取消冒泡后,颜色便不会再往外填充
level4.addEventListener('click', (e)=>{
e.stopPropagation()
removeX(e)
})
不可阻止默认动作
注意,有些事件是不能阻止默认动作的 我们搜索scroll_event_MDN,可以看到Bubble和cancelable
- Bubbles就是该事件是否冒泡,所有冒泡事件都可以取消
- Cancelable的意思是开发者是否可以阻止默认事件
- Cancelable与冒泡无关
一个可能会用到的小技巧
如何阻止滚动?
我们知道,scroll事件不可阻止默认动作,但我就是叛逆,就是不想让它滚动,应该怎么做?
x.addEventListener('wheel', (e)=>{
e.preventDefault()
})//阻止滑轮滑动滚动条
x.addEventListener('touchstart', (e)=>{
e.preventDefault()
})//阻止手机端滑动页面
并在CSS中将滚动条隐藏
::-webkit-scrollbar {
width: 0 !important}
}
这样就能阻止页面滚动
事件委托
什么是事件委托?
事件代理又叫事件委托,JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
通常,事件委托即是把原本需要绑定在子元素的响应事件委托给祖先元素,让祖先元素担当事件监听的职务;这样只需监听一个祖先元素,就能同时处理多个子元素的事件。
使用场景举例
场景一:如果要给100个按钮添加点击事件,应该怎么做?
当然,可以给100个按钮都绑定事件,但这样太过于耗费内存,且不够简洁,
所以,利用事件委托,我们可以监听这100个按钮的祖先,等冒泡的时候判断target是不是这100个按钮中的一个
场景二:你要监听目前不存在的元素的点击事件(比如利用setTimeout,在数秒之后再添加按钮),应该怎么做?
答案依然是利用事件委托,监听祖先,等点击的时候看看是不是你想要监听的元素即可
从上述场景可以看出事件委托的优点:
- 省监听数(内存)
- 可以监听动态元素
封装事件委托
一个简单的需要:用户点击了li标签里的数字,就打印出"li被点击了"
<ul id="testDiv">
<li>1<li>
<li>2<li>
<li>3<li>
<li>4<li>
<li>5<li>
</ul>
利用事件委托:
testDiv.addEventListener('click', (e)=> {
const t = e.target //把t记为被用户操作的元素
if(t.matches('li')) {//判断t是不是li
console.log('li被点击了')
}
})
思路:
- 监听父元素
- 设置一个t,把t记为被用户操作的元素
- 判断被用户操作的t是不是li元素,如果是,打印出值
根据这样的思路,我们封装一个事件委托,
只需写出on('click','#testDiv','li',fn)
当用户点击#testDiv里的li中的数字,就调用fn
function on(eventType, element, selector, fn) {
if (!(element instanceof Element)) {
//判断传进来的element是不是Element(元素),如果不是,如#testDiv,就把它变成一个元素
element = document.querySelector(element)
}
element.addEventListener(eventType, (e)=>{
const t = e.target
if (t.matches(selector)) {
fn(e)
}
})
}
const f1 = ()=>{
console.log('li被点击了')
}
on('click','#testDiv','li',f1)
但这样的操作,有一个问题:
<ul id="testDiv">
<li><span><p>1<p><span><li>
<li><span>2<span><li>
<li><span>3<span><li>
<li><span>4<span><li>
</ul>
如果被点击的元素被多个祖先元素包住,这个方法就不管用了
更改思路:
- 递归地往外找祖先元素,直到找到li元素为止
- 当然不能无止境的往上找,如果到了监听的元素(上面为ul)还没找到li,就要停止查找
function on(eventType, element, selector, fn) {
if (!(element instanceof Element)) {
element = document.querySelector(element)
}
element.addEventListener(eventType, (e)=>{
let el = e.target
while(!el.matches(selector)){
if (element === el){
//在往上找的过程中,如果当前元素已经到testDiv了,默认找不到了,就把el为空,停止查找
el = null
break
}
//判断被操作的元素是否符合li,不符合就将它等于它的爸爸
el=el.parentNode
}
el&&fn.call(el,e,el)//找到参数匹配选择器,就执行函数
})
}
const f1 = ()=>{
console.log('li被点击了')
}
on('click', '#testDiv', 'li',f1)