DOM事件与事件委托
高频面试题目
1.点击事件
- 代码
<div class="爷爷">
<div class="爸爸">
<div class="儿子">
文字
</div>
</div>
</div>
- 给三个div分别添加时间监听 fnYe/fnBa/fnEr
- 提问1:点击了谁?
1.点击文字,算不算点击儿子?
2.点击文字,算不算点击爸爸?
3.点击文字,算不算点击爷爷?
答:都算 -
提问2:调用顺序
点击文字,最先调用fnYe/fnBa/fnEr哪个函数?
答:都行(对于顺序IE5和网景有争论,由W3C发布规矩)
2.调用顺序
2.1 W3C发布标准(函数调用顺序)
- 文档明为DOM Level 2 Event Specifition
- 规定浏览器应该支持两种调用顺序
- 首先按 爷爷=>爸爸=>儿子 顺序看有没有函数监听
- 然后按 儿子=>爸爸=>爷爷 顺序看有没有函数监听
- 先 从外到内 再 从内到外
- 有监听函数就调用,并提供时间信息,没有就跳过
2.2 术语
- 从外向内找监听函数,叫事件捕获
- 从内向外找监听函数,叫事件冒泡
2.3 提问:那岂不是 fnYe / fnBa / fnEr 都调用两次?非也!
解决:开发者自己选择把监听函数放到捕获阶段还是冒泡阶段
3.addEventListener
- 事件绑定API
IE5*:baba.attachEvent('onclick',fn)
//冒泡
网景:baba.addEventListener('click',fn)
//捕获
W3C:baba.addEventListener('click',fn,bool)
(bool参数用来选择是冒泡还是捕获,如果不填bool就选择IE的冒泡) - 如果 bool 不填或为 falsy(类似false的值)
就让 fn 走冒泡,即当浏览器在冒泡阶段发现 baba 有 fn 监听函数,就会调用 fn,并提供时间信息(W3C默认偏向IE,因为经常默认不填bool) - 如果 bool 为 true
就让 fn 走捕获,即当浏览器在捕获阶段发现 baba 有 fn 监听函数,就会调用 fn,并提供时间信息 - 注意:捕获和冒泡都是一定要走的,只是决定在哪里执行函数
4.案例
- 代码实现:颜色从内往外依次出现
原理是用 x 设置了背景透明,然后依次删掉每个level的x - JS渲染是一个很快的过程,每一个间隔的操作只相差很短的时间
- 背景透明(CSS)
.x{background: transparent};
- fn的参数e只在点击的一瞬间存在,点击完了之后参数就不存在了
- 解决:const 一个变量 t,令 t=e.currentTarget
这样,即使点击一瞬间e消失了,t仍然存在 - 如果几个level的setTimeout时间都是一样是1000的话,就相当于在1s后添加了7个闹钟,想要依次响起就令为 n*1000 然后 n+=1
- 如果没有传入 bool 参数,则默认冒泡顺序,也就是执行顺序是从内往外
- 如果想要捕获的过程:则每个都添加一个 true
- 想要两个一起看,则绑定两个事件,则一共14个监听函数
先是捕获(从外到内),再是冒泡(从内到外)
let n=1
level1.addEventListener('click',(e)=>{
const t = e.currentTarget
setTimeout(()=>{
t.classList.remove('x')//删掉x
}, n*1000)
})//没有加 bool 参数默认为冒泡顺序,从里向外
level1.addEventListener('click',(e)=>{
const t = e.currentTarget
setTimeout(()=>{
t.classList.add('x')//添加上刚刚删掉的x
},n*1000)
},true)//捕获顺序
- 代码优化
将重复的代码取一个名字。(比如:给个函数,然后调用它即可)
let n=1
const fn = (e)=>{
const t = e.currentTarget
setTimeout(()=>{
t.classList.remove('x')//删掉x
}, n*1000)
}
const fm = (e)=>{
const t = e.currentTarget
setTimeout(()=>{
t.classList.add('x')//添加上刚刚删掉的x
},n*1000)
}
level1.addEventListener('click',fn)
level1.addEventListener('click',fm,ture)
4.1 代码图解
- 每次点击都会从 window ~ 文字,然后从 文字 ~ window
- 进入的时候发现 level1 上面有个函数,就执行,从f1-f7,然后从f7~f1
- 可以每个上面都绑定同一个函数
5.小结
5.1两个疑问
- 儿子被点击了,算不算点击了老子?
答:算,只要div里面任何一部分被点击了,这个div就算被点击了 - 那么先调用的老子的函数还是先调用儿子的函数?
答:不一定
5.2 捕获与冒泡
- 捕获说:先调用爸爸的监听函数
- 冒泡说:先调用儿子的监听函数
5.3 W3C 事件模型
- 先捕获(先爸爸=>儿子),再冒泡(再儿子=>爸爸)
注意:后面可以阻止冒泡 - 注意e对象被传给所有监听函数
- 事件结束后,e对象就不存在了
6.target V.S. currentTarget
6.1 区别
- e.target - 用户在操作的元素
e对象的属性 target - e.currentTarget - 程序员监听的函数
- this是 e.currentTarget ,监听代码里不推荐使用它
6.2 举例
- div > span{文字},用户点击文字
- e.taeget 就是span
因为用户点的是 span 上的文字 - e.currentTarget 就是div
程序员监听的是div,div.onclick
7.一个特例
- 只有一个 div 被监听(不考虑父子同时被监听)
- fn 分别在捕获阶段和冒泡阶段监听click时间
- 用户点击的就是监听的元素,没有爸爸和儿子的包含关系,那么谁先监听谁就先执行,没有父子。
level7.addEventListener('click',()=>{
console.log(2)
},true)//捕获
level7.addEventListener('click',()=>{
console.log(1)
})//冒泡
8.取消冒泡
- 捕获不能取消,但是冒泡可以
- e.stopPropagation()可以中断冒泡,浏览器不再往上走
- 通俗来说:有人打我,我自己解决,别告诉我老子
- 一般用于封装独立组件
level4.addEventListener('click',(e)=>{
e.stopPropagation()
fm(e)
})
9.不可取消冒泡
- 有些事件不可(不支持)取消冒泡
- MDN 搜索 scroll event(滚动事件),看到 Bubbles 和 Cancelable
- Bubbles 的意思是该事件是否冒泡
- Cancelable 的意思是开发者是否可以取消冒泡
10.如何阻止滚动
10.1 scroll 事件不可取消冒泡
- 阻止 scroll 默认动作没用,因为现有滚动才有滚动事件
- 要阻止滚动,可以阻止 wheel(滚轮滚动) 和 touchstart(手机触屏) 的默认动作
- 注意你需要找准滚动条所在的元素
要找到能覆盖整个区域的元素 - 但是滚动条鼠标点击还能用,可用CSS让滚动条 width: 0
10.2 CSS也行
- 使用 overflow: hidden 可以直接取消滚动条
- 但此时 JS 依然可以修改 scrollTop
::-webkit-scrollbar { width:0 !important}
x.addEventListener('wheel',(e)=>{//x为body的id
e.preventDfault()
})
x.addEventListener('touchstart',(e)=>{
e.preventDfault()
})
11 小结
- target 和 currentTarget
一个是用户点击的,一个是开发者监听的 - 取消冒泡
e.stopPropagation() - 事件的特性
Bubbles 表示是否冒泡
Cancelable 表示是否支持开发者取消冒泡
如 scroll 不支持冒泡 - 如何禁用滚动
取消特定元素的 wheel 和 touchstart 的默认动作
而不是阻止冒泡