# 事件
DOM 事件标准描述了事件传播的 3 个阶段:
捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
目标阶段(Target phase)—— 事件到达目标元素。
冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。
粟子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
div{
padding: 50px;
background: #3eaf7c;
text-align: center;
}
span{
display: inline-block;
width: 100px;
height: 50px;
background: #4a67de;
}
</style>
</head>
<body>
<div>
<span></span>
</div>
</body>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => console.log(`Bubbling: ${elem.tagName}`));
}
</script>
</html>
当我们点击元素 span
时,控制台输出:
Bubbling: SPAN
Bubbling: DIV
Bubbling: BODY
Bubbling: HTML
发现显示的结果从里到外,也就是冒泡阶段,因为默认情况下事件处理的触发就是在冒泡阶段
为了查看捕获事件效果的,我们改下事件绑定的方式
elem.addEventListener("click", e => console.log(`Bubbling: ${elem.tagName}`), true);
此时我们再次点击元素 span
时,控制台输出:
Capturing: HTML
Capturing: BODY
Capturing: DIV
Capturing: SPAN
也可以将冒泡和捕获一起输出
elem.addEventListener("click", e => console.log(`Capturing: ${elem.tagName}`), true);
elem.addEventListener("click", e => console.log(`Bubbling: ${elem.tagName}`))
此时打印结果为:
Capturing: HTML
Capturing: BODY
Capturing: DIV
Capturing: SPAN
Bubbling: SPAN
Bubbling: DIV
Bubbling: BODY
Bubbling: HTML
# 阻止冒泡的捕获
e.stopPropagation(): 阻止捕获和冒泡阶段中当前事件的进一步传播。但是,它不能防止任何默认行为的发生; 例如,对链接的点击仍会被处理
function handleDIV(e) {
console.log('handleDIV')
}
function handleSPAN(e) {
console.log('handleSPAN')
e.stopPropagation()
}
function handleOther(e) {
console.log('handle', e.currentTarget.tagName)
}
for(let elem of document.querySelectorAll('*')) {
let fn = window['handle'+elem.tagName] ||handleOther
elem.addEventListener("click", fn);
}
此时点击元素 span
时,控制台只会输出:
handleSPAN
说明阻止了事件在冒泡阶段的传播
function handleDIV(e) {
console.log('handleDIV')
}
function handleSPAN(e) {
console.log('handleSPAN')
}
function handleOther(e) {
console.log('handle', e.currentTarget.tagName)
e.stopPropagation()
}
for(let elem of document.querySelectorAll('*')) {
let fn = window['handle'+elem.tagName] ||handleOther
elem.addEventListener("click", fn, true);
}
将粟子改成捕获的形式,此时点击元素 span
时,控制台只会输出:
handle HTML
说明阻止了事件在捕获阶段的传播
# 阻止默认行为
preventDefault()
如果此事件没有被显式处理,它默认的动作也不应该照常执行。此事件还是继续传播,除非碰到事件侦听器调用 stopPropagation()
或 stopImmediatePropagation()
,才停止传播
<a href="www.baidu.com">www.baidu.com</a>
<script >
function handleA(e) {
console.log('handleA')
e.preventDefault()
}
</script>
此时点击 a
标签,将不会触发跳转行为
# target和currentTarget
currentTarget
:表示当前绑定事件事件处理器的所在元素target
:表示触发事件元素
上文提到因为事件存在冒泡和捕获阶段,所以一个元素的绑定事件的触发不一定是当前这个元素
<body>
<div>
<span></span>
</div>
</body>
<script>
function handleDIV(e) {
console.log('handleDIV-target', 'target:', e.target.tagName, 'currentTarget', e.currentTarget.tagName)
}
function handleSPAN(e) {
console.log('handleSPAN-target', 'target:', e.target.tagName, 'currentTarget', e.currentTarget.tagName)
}
function handleOther(e) {
console.log('handle', e.currentTarget.tagName, 'target:', e.target.tagName, 'currentTarget', e.currentTarget.tagName)
}
for(let elem of document.querySelectorAll('*')) {
let fn = window['handle'+elem.tagName] ||handleOther
// elem.addEventListener("click", fn, true);
elem.addEventListener("click", fn);
}
</script>
点击 span
元素时,输出结果为:
handleSPAN-target target: SPAN currentTarget SPAN
handleDIV-target target: SPAN currentTarget DIV
handle BODY target: SPAN currentTarget BODY
handle HTML target: SPAN currentTarget HTML
因为我们点击的 span
元素,所以 target
都是 span
元素, currentTarget
为绑定事件所在的元素
# 事件委托
事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托(事件代理)。
使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理
# 事件委托的特点
减少内存消耗
果有一个列表,列表之中有大量的列表项,需要在点击列表项的时候响应一个事件:
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件时再去匹配判断目标元素,所以事件委托可以减少大量的内存消耗,节约效率
动态绑定事件
给上述的例子中每个列表项都绑定事件,在很多时候,需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的,所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。
# 局限性
当然,事件委托也是有局限的。比如 focus
、blur
之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemove
、mouseout
这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的