事件
通俗理解,事件是用户或者浏览器自己执行的某种动作,是文档或者浏览器发生的一些交互瞬间,比如点击(click)按钮等,这里的click就是事件的名称。JS与html之间的交互是通过事件实现的。
每个事件都有事件监听器(有时也叫事件监听器),也就是触发事件时运行的代码块。严格来说事件监听器监听事件是否发生,然后事件处理器对事件做出反应。
事件模型
一个事件发生后,会在子元素及父元素之间进行传播(propagation),这种传播分为三个阶段。
DOM事件传播的三个阶段:
- 捕获阶段
- 目标阶段
- 冒泡阶段
事件捕获和事件冒泡
- 事件捕获:由外向内找监听函数。
- 事件冒泡:由内向外找监听函数。
例子:
<div class="grandfather">
<div class="father">
<div class="son"></div>
hi
</div>
</div>
//假如给三个div分别添加事件监听 fnYe / fnBa / fnEr
问题一:
- 点击文字,算不算点击儿子?
- 点击文字,算不算点击爸爸?
- 点击文字,算不算点击爷爷? 答案:都算
问题二: 调用监听函数顺序是什么呢?
事件捕获:fnYe > fnBa > fnEr , 也就是从外到内去调用
事件冒泡:fnEr > fnBa > fnYe , 也就是从内到外去调用
W3C在2002年发布了标准, 文件名为DOM Level 2 Events Specification
规定浏览器同时支持两种调用顺序
首先按爷爷->爸爸->儿子顺序看有没有函数监听 , 先事件捕获
然后按儿子->爸爸->爷爷顺序看有没有函数监听 , 再事件冒泡
如图所示:
如何指定走捕获还是冒泡呢?
baba.attachEvent('onclick',fn)//冒泡
baba.addEventListener('click',fn)//捕获
baba.addEventListener('click',fn,bool)//w3c制定
- 如果bool不传或为falsy
- 就让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;
}
JS:
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) //true 走捕获
level1.addEventListener('click',fa) //默认为 false 走冒泡
// 先捕获后冒泡
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)
一个特例
//只有一个 div 被监听(不需要考虑父子关系)
div.addEventListenter('click',f1) //冒泡
div.addEventListenter('click',f2,true) //捕获
f1
先执行还是 f2
先执行呢?
先捕获在冒泡, f2
先执行?
正确答案:f1
先执行。
当没有父子关系时,谁先监听谁就先执行。
target 与 currentTarget的区别
e.target
用户操作的元素e.currentTarget
程序员监听的元素
例子:
<div>
<span>文字</span>
</div>
e.target
就是span
,用户操作的元素。e.currentTarget
就是div
, 程序员监听的元素。
取消冒泡
e.stopPropagation()
可打断冒泡,浏览器不再向上走
一般用于封装某些独立组件
注意:捕获不可以取消但是冒泡可以(有些事件也不能够取消冒泡,例如: scroll
滚动条)
事件委托
事件委托的好处:
- 省内存(省监听数)
- 可以监听动态元素
例子一:
//有 100 个 button
<div id="div1">
<span>span1</span>
<button>click 1</button>
<button>click 2</button>
<button>click 3</button>
<button>click 4</button>
<button>click 5</button>
.
.
.
<button>click 100</button>
</div>
如果有100个 button
怎么办呢,不可能创建 100 个监听器吧。
将监听委托给 div
我们只需要监听这个100个按钮的祖先,等冒泡的时候判断 e.target
是不是这100个按钮中的一个,节省内存。
div1.addEventListener('click',(e)=>{
const t = e.target
if(t.tagName.toLowerCase()==='button'){
console.log('button 被点击了')
}
})
例子二:
当 button
延迟1秒后才出现,我们如何监听呢?
// 延迟1秒后,创建 button
setTimeout(()=>{
const button = document.createElement('button')
button.textContent='click'
div1.appendChild(button)
},1000)
div1.addEventListener('click',(e)=>{
const t = e.target
if(t.tagName.toLowerCase()==='button'){
console.log('button被点击')
}
})
监听祖先,等点击的时候看看是不是监听的元素即可,监听动态元素。
封装事件委托
setTimeout(()=>{
const button = document.createElement('button')
button.textContent='click'
div1.appendChild(button)
},1000)
on('click','#div1','button',()=>{ //'#div'是选择器不是元素
console.log('button 被点击啦')
})
// 声明 on 函数
function on(eventType,element,selector,fn){
//判断 element 类型是不是元素
if(!(element instanceof Element)){
// 不是则找到指定的元素并赋值给 element
element = document.querySelector(element)
}
element.addEventListener(eventType,(e)=>{
const t= e.target
if(t.matches(selector)){
fn(e)
}
})
}