目录 § ⇧
- 1. 点击事件 §
- 2.
addEventListener
§
- 3.
target
V.S. currentTarget
§
- 4. 取消冒泡 §
- 5. 不可取消冒泡的事件 §
- 6. 阻止滚动 §
- 7. 自定义事件 §
- 8. 事件委托(事件代理) §
- 9. 封装事件委托 §
- 10. $3 §
- 11. $3 §
1. 点击事件 § ⇧
有三个div,.爷爷>.爸爸>.儿子
- 给三个div分别添加事件监听,
fnYe/fnBa/fnEr
1
2
3
4
5
6
7
|
<div class="爷爷">
<div class="爸爸">
<div class="儿子">
文字
</div>
</div>
</div>
|
- 点击文字,同时算点击了儿子、爸爸、爷爷
- 点击文字,事件的调用顺序
- W3C制定浏览器标准需同时支持两种调用顺序
- 首先按爷爷->爸爸->儿子顺序查找函数监听,从外向内,找监听函数,叫事件捕获
- 然后按儿子->爸爸->爷爷顺序查找函数监听,从内向外,找监听函数,叫事件冒泡
- 有监听函数就调用,并提供事件信息,没有就跳过
- 可选择间监听的函数放在捕获阶段还是冒泡阶段调用
2.addEventListener
§ ⇧
2.1 事件绑定API
element.addEventListener('click', fn, boolean)
冒泡
如果bool不传或为falsy
- 就让fn走冒泡,即当浏览器在冒泡阶段发现节点有fn监听函数,就会调用fn,并提供事件信息
- 即监听函数默认走冒泡
如果bool为true
- 就让fn走捕获, 即当浏览器在捕获阶段发现节点有fn监听函数,就会调用fn,并提供信息
你可以选择吧fn放在哪边
- 如果bool为true,fn就放在左边,右边空跑
- 如果bool为空或falsy,fn就放在右边,左边空跑
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<body>
<div class="lv1 x">
<div class="lv2 x">
<div class="lv3 x">
<div class="lv4 x">
<div class="lv5 x">
<div class="lv6 x">
<div class="lv7 x"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="main.js"></script>
</body>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
* {
box-sizing: border-box;
}
div[class^="lv"]:not([class=".lv7"]) {
border: 1px solid;
border-radius: 50%;
display: inline-flex;
padding: 10px;
}
.lv1 {
background-color: purple;
}
.lv2 {
background-color: blue;
}
.lv3 {
background-color: cyan;
}
.lv4 {
background-color: green;
}
.lv5 {
background-color: yellow;
}
.lv6 {
background-color: orange;
}
.lv7 {
width: 50px;
height: 50px;
border: 1px solid;
border-radius: 50%;
background-color: red;
display: inline-flex;
}
.x {
background-color: transparent;
}
|
1
2
3
4
5
6
7
8
9
10
11
|
const $ = document.querySelector.bind(document)
const [lv1, lv2, lv3, lv4, lv5, lv6, lv7] = [$('.lv1'), $('.lv2'), $('.lv3'), $('.lv4'), $('.lv5'), $('.lv6'), $('.lv7')]
let n = 1
console.log(lv1)
lv1.addEventListener('click', (e) => {
console.log(e.currentTarget)
setTimeout(() => {
console.log(e.currentTarget)
e.currentTarget.classList.remove('x');
}, n * 1000)
})
|
事件结束后,e 还在?
- 答案并不是「在」,也不是「不在」
- e 只是没了
currentTarget: null
,其他属性都还「健在」
- 动作结束,事件结束,e 会被浏览器悄悄改变
- 不推荐在事件结束后访问 e
- 把属性复制到自己的变量里再使用,比如
const tempEvent = e.currentTarget
就是把 currentTarget 的引用复制到 tempEvent
1
2
3
4
5
6
7
8
9
10
11
|
const $ = document.querySelector.bind(document)
const [lv1, lv2, lv3, lv4, lv5, lv6, lv7] = [$('.lv1'), $('.lv2'), $('.lv3'), $('.lv4'), $('.lv5'), $('.lv6'), $('.lv7')]
let n = 1
console.log(lv1)
lv1.addEventListener('click', (e) => {
const tempEvent = e.currentTarget
setTimeout(() => {
tempEvent.classList.remove('x');
}, n * 1000)
n += 1
})
|
循环监听事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
const $ = document.querySelector.bind(document)
let lvn = [lv1, lv2, lv3, lv4, lv5, lv6, lv7] = [$('.lv1'), $('.lv2'), $('.lv3'), $('.lv4'), $('.lv5'), $('.lv6'), $('.lv7')]
const removeX = (e) => {
const tempEvent = e.currentTarget
setTimeout(() => {
tempEvent.classList.remove('x');
}, n * 100)
n += 1
}
const addX = (e) => {
const tempEvent = e.currentTarget
setTimeout(() => {
tempEvent.classList.add('x');
}, n * 150)
n += 1
}
let n = 1
for (let i = 0; i < lvn.length; i++) {
const lvnEle = lvn[i];
lvnEle.addEventListener('click', removeX, true)
lvnEle.addEventListener('click', addX, false)
// console.log(n, i)
}
|
事件捕获和冒泡效果链接
2.2 小结
注意点
- 子节点被点击了,也算点击父节点
- 先调用父节点的函数还是先调用儿子的函数
捕获与冒泡
- 捕获先调用父节点的监听函数
- 冒泡先调用子节点的监听函数
W3C 事件模型
- 先捕获(先爸爸->儿子),再冒泡(先儿子->爸爸)
- 冒泡可以被阻止,而捕获不可被阻止
- 注意 e 对象被传给所有监听函数
- 事件结束后,e 对象就不存在了
3.target
V.S. currentTarget
§ ⇧
区别
e.target
用户操作的元素
e.currentTarget
程序员监听的函数
this
是e.currentTarget
,不推荐使用,易混淆
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// <div id="target">target</div>
console.log(target.onclick) // null
// 简单赋值 改写 (箭头函数不支持内部 this)
target.onclick = function(e){
console.log('hi')
console.log(this)
console.log(e) // 包含了点击事件的所有信息
}
// target.onclick.call(target, event)
// this <- target
// event <- e
// this 是浏览器在用户点击时,用 .call() 按顺序传进来的
|
举例
- div > span{文字},用户点击文字
e.target
就是span
e.currentTarget
就是div
特例:同一个节点上的监听事件
- 背景
- 只有一个div被监听(不考虑父子同时被监听),同级
- fn分别在捕获阶段和冒泡阶段监听click事件
- 用户点击的元素就是开发者监听的
e.target === e.currentTarget
- 代码
div.addEventListener('click', f1)
div.addEventListener('click',f2, true)
- 问,f1 先执行还是 f2 先执行
- 如果把位置调换后,哪个先执行
- 哪个先监听就哪个先执行
4. 取消冒泡 § ⇧
捕获不可取消,但冒泡可以
e.stopPropagation()
可以中断冒泡,浏览器不再向上传递
- 一般用于封装某些独立的组件
5. 不可取消冒泡的事件 § ⇧
- MDN 英文版搜
scroll event
,看到Bubbles
和Cancelable
Bubbles
是该事件是否会冒泡
Cancelable
是开发者是否可以取消冒泡
6. 阻止滚动 § ⇧
6.1 阻止页面滚动的步骤
scroll
事件不可取消冒泡和默认事件
- 阻止
scroll
默认事件无效,因先有滚轮滚动才有滚动事件
- 要阻止滚动,可阻止
wheel
和touchstart
的默认动作
1
2
3
4
5
6
7
|
x.addEventListener('wheel', (e) => {
e.preventDefault()
});
x.addEventListener('touchstart', (e) => {
e.preventDefault()
});
|
- 注意需要找准滚动所在的元素,能覆盖整个滚动区域的元素,示例代码
- 但是滚动条依然开可以用鼠标拖动,可以用CSS让滚动条
width: 0
1
2
3
|
::-webkit-scrollbar {
width: 0 !important;
}
|
CSS
实现隐藏滚动条
- 滚动条是在
Document
上的
- 使用
overflow:hidden
可以直接取消滚动条
- 但此时 JS 依然可以修改
scrollTop
在有iframe
的场景下可能失效
6.2 小结二
target
和currentTarget
取消冒泡
事件的特性
Bubbles
Cancelable
scroll
的Cancelable
是false
如何禁用滚动
- 取消特定元素的
wheel
和touchstart
的默认动作
7. 自定义事件 § ⇧
浏览器自带事件
自定义事件
const userEvent = new CustomEvent("newName", {detail:{}, bubbles, cancelable})
userElement.dispatchEvent(event)
userElement.addEventListener(event)
- 示例代码
8. 事件委托(事件代理) § ⇧
- 让父/祖元素监听原本在子元素上的监听事件;将子元素监听事件委托父/祖元素监听
场景一
- 要给100个按钮添加点击事件,不需要添加100个监听器
- 监听这100元素的祖先元素,等冒泡的时候判断是否传递到当前元素
- 即判断用户操作的
target
是否是这100个按钮中的一个
场景二
- 要监听目前不存在的元素的点击事件
- 监听祖先元素,等点击的时候看是否是想要监听的元素
事件委托的优点
示例代码链接
9. 封装事件委托 § ⇧
要求
- 写出这样一个函数
on('click', '#testDiv', 'li', fn)
- 当用户点击
#testDiv
里的li
元素时,调用fn函数
- 事件委托
方案一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
/*
<div id="div1">
<span>span1</span>
</div>
<script src="EventDelegation.js"></script>
*/
/* // 未封装
div1.addEventListener('click', (e) => {
const tempButton = e.target // by user
if (tempButton.tagName.toLowerCase() === "button") {
console.log('button被点击了')
console.log('button内容是' + tempButton.textContent)
console.log('button id是' + tempButton.dataset.id)
}
});
*/
for (let i = 0; i < 10; i += 1) {
setTimeout(() => {
const button = document.createElement('button')
button.textContent = 'click ' + i
button.setAttribute("data-id", i)
div1.appendChild(button)
}, i * 1000)
}
on('click', '#div1', 'button', (e) => {
const tempElement = e.target // by user
console.log('button被点击了')
console.log('button内容是' + tempElement.textContent)
console.log('button id是' + tempElement.dataset.id)
})
function on(eventType, element, selector, fn) {
if (!(element instanceof Element)) {
element = document.querySelector(element)
}
element.addEventListener(eventType, (e) => {
const tempElement = e.target // by user
if (tempElement.matches(selector) === true) {
fn(e)
}
})
}
|
- 这个答案有缺陷
- 如果将操作的元素内部再放一层子元素,比如包裹
span
元素
- 操作的元素变为
span
元素
- 点击的时候,
tempElement
是span
,而selector
是button
,不匹配,就不执行fn
方案二
- 递归判断
target/target的父元素/target的祖元素
是否匹配li
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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)) {
// 范围是 element 之内的元素
if (element === el) {
el = null
break
}
el = el.parentNode
}
el && fn.call(el, e, el)
})
return element
}
|
jQuery
的实现
$('#xxx').on('click', 'li', fn)
JS 支持事件吗
- DOM 事件不属于JS的功能,而是浏览器提供的 DOM 功能
- JS 只是调用了 DOM 提供的
addEventListener
这个API
- 所以 JS 不支持,提供事件机制的时浏览器
追问:手写JS事件系统
面试题:请简述 DOM 事件模型或 DOM 事件机制(你可以写一篇博客来回答这个问题)
提示:请描述什么是捕获什么是冒泡,并说说是先捕获还是先冒泡,再搜搜看网上的文章怎么说
- 浏览器的事件模型,就是通过监听函数(listener) 事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数
- DOM 事件机制 事件发生会分成三个阶段 捕获 目标 冒泡阶段,在各层元素之间传播,使得同一个事件会在多个节点上触发
- 事件捕获是从window(-> Document -> html -> …)开始由外向内查找监听函数
- 事件冒泡是从当前操作的节点(目标节点)开始由内向外直至 window 查找监听函数
- 按照先捕获,后冒泡的顺序执行
捕获:当用户点击按钮,浏览器会从 window 从上向下遍历至用户点击的按钮,逐个触发事件处理函数。
冒泡:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。
W3C 事件模型/事件机制:对每个事件先捕获再冒泡
面试题:请简述事件委托(可写博客回答)
提示:请想办法把「监听祖先元素」说得高大上一点
- 事件委托就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素
- 一般是把元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素
- 当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件
- 然后在外层元素上去执行函数,即便一开始当前元素不存在于页面中,也能实现事件委托
- 以达到减少内存消耗和动态绑定事件的目的
事件委托就是把事件监听放在祖先元素(如父元素、爷爷元素)上。
好处是:1 节约监听数量 2 可以监听动态生成的元素。
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 错误版(但是可能能过)
ul.addEventListener('click', function(e){
if(e.target.tagName.toLowerCase() === 'li'){
fn() // 执行某个函数
}
})
// bug 在于,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对。
// 高级版
function delegate(element, eventType, selector, fn) {
element.addEventListener(eventType, e => {
let el = e.target
while (!el.matches(selector)) {
if (element === el) {
el = null
break
}
el = el.parentNode
}
el && fn.call(el, e, el)
})
return element
}
// 思路是点击 span 后,递归遍历 span 的祖先元素看其中有没有 ul 里面的 li。
|
参考文章
相关文章
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名
- 河
掘
思
知
简