【事件传播】

事件冒泡

事件冒泡概念:
当一个元素的事件被触发时,同样的事件将会在该元素的所有祖先元素中依次被触发。这一过程被称为事件冒泡
简单理解:当一个元素触发事件后,会依次向上调用所有父级元素的同名事件
事件冒泡是默认存在的

鼠标经过事件:
mouseovermouseout 会有冒泡效果
mouseentermouseleave 没有冒泡效果(推荐)

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
49
50
51
52
53
54
55
56
57
58
59
	<style type="text/css">
#box1{
width: 200px;
height: 200px;
background-color: yellowgreen;
}

#s1{
background-color: yellow;
}


</style>
<script type="text/javascript">

window.onload = function(){

/*
* 事件的冒泡(Bubble)
* - 所谓的冒泡指的就是事件的向上传导,当后代元素上的事件被触发时,其祖先元素的相同事件也会被触发
* - 在开发中大部分情况冒泡都是有用的,如果不希望发生事件冒泡可以通过事件对象来取消冒泡
*
*/

//为s1绑定一个单击响应函数
var s1 = document.getElementById("s1");
s1.onclick = function(event){
event = event || window.event;
alert("我是span的单击响应函数");

//取消冒泡
//可以将事件对象的cancelBubble设置为true,即可取消冒泡,这样祖先元素相同事件就不会被触发
event.cancelBubble = true;
};

//为box1绑定一个单击响应函数
var box1 = document.getElementById("box1");
box1.onclick = function(event){
event = event || window.event;
alert("我是div的单击响应函数");

/*因为默认就有冒泡模式的存在,所以容易导致事件影响到父级元素。若想把事件就限制在当前元素内,就需要阻止事件流动阻止事件流动需要拿到事件对象。
此方法可以阻断事件流动传播,不光在冒泡阶段有效,捕获阶段也有效*/
// e.stopPropagation()
};

//为body绑定一个单击响应函数
document.body.onclick = function(){
alert("我是body的单击响应函数");
};
};

</script>
<body>

<div id="box1">
我是box1
<span id="s1">我是span</span>
</div>

阻止冒泡

阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。

stopPropagation方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。

1
2
3
4
5
function stopEvent(e) {
e.stopPropagation();
}

el.addEventListener('click', stopEvent, false);

上面代码中,click事件将不会进一步冒泡到el节点的父节点。

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
<body>
<h3>阻止冒泡</h3>
<p>阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。</p>
<div class="outer">
<div class="inner">
<div class="child"></div>
</div>
</div>
<script>
// 获取嵌套的3个节点
const outer = document.querySelector('.outer')
const inner = document.querySelector('.inner')
const child = document.querySelector('.child')

// 外层的盒子
outer.addEventListener('click', function () {
console.log('outer...')
})

// 中间的盒子 inner.addEventListener('click', function (ev) {
console.log('inner...')

// 阻止事件冒泡
ev.stopPropagation()
})

// 内层的盒子
child.addEventListener('click', function (ev) {
console.log('child...')

// 借助事件对象,阻止事件向上冒泡
ev.stopPropagation()
})
</script>
</body>

结论:事件对象中的 e.stopPropagation 方法,专门用来阻止事件冒泡。

阻止默认事件

e.preventDefault() 方法用来阻止事件产生的 “默认动作”。

一些特殊的业务需求,需要阻止事件的 “默认动作”。

【小案例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
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<p>
只能输入小写字母和数字:
<input type="text" id="field">
</p>
<script>
var oField = document.getElementById('field');

oField.onkeypress = function (e) {
console.log(e.charCode);

// 根据用户输入的字符的字符码(e.charCode)
// 数字 0~9,字符码 48~57
// 小写字母 a~z,字符码 97~122
if (!(e.charCode >= 48 && e.charCode <= 57 || e.charCode >= 97 && e.charCode <= 122)) {
// 阻止浏览器的默认行为
e.preventDefault();
}
};
</script>
</body>

</html>

style=“zoom: 33%;” />

【小案例2】

制作鼠标滚轮事件:当鼠标在盒子中向下滚动时,数字加 1;反之,数字减 1。

鼠标滚轮事件是 onwheel,它的事件对象 e 提供 deltaY 属性表示鼠标滚动方向,向下滚动是返回正值,向上滚动时返回负值。

  • 没有阻止事件的 “默认动作” 时
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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box {
width: 200px;
height: 200px;
background-color: #333;
}

body {
height: 2000px;
}
</style>
</head>

<body>
<div id="box"></div>
<h1 id="info">0</h1>

<script>
var oBox = document.getElementById('box');
var oInfo = document.getElementById('info');

// 全局变量就是 info 中显示的数字
var a = 0;

// 给 box 盒子添加鼠标滚轮事件监听
oBox.onwheel = function (e) {
if (e.deltaY > 0) {
a++;
} else {
a--;
}
oInfo.innerText = a;
}
</script>
</body>

</html>

style=“zoom: 33%;” />

  • 阻止事件的 “默认动作” 后
1
2
3
4
5
6
7
8
9
10
11
12
// 给 box 盒子添加鼠标滚轮事件监听
oBox.onmousewheel = function (e) {
// 阻止默认事件:就是说当用户在盒子里面滚动鼠标滚轮的时候,此时不会引发页面的滚动条的滚动
e.preventDefault();

if (e.deltaY > 0) {
a++;
} else {
a--;
}
oInfo.innerText = a;
}

style=“zoom:33%;” />

事件委托

批量添加事件监听

题目:页面上有一个无序列表 <ul>,它内部共有 20 个 <li> 元素,请批量给它们添加事件监听,实现效果:点击哪个 <li> 元素,哪个 <li> 元素就变红。

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<ul id="list">
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
</ul>

<script>
var oList = document.getElementById('list');
var lis = oList.getElementsByTagName('li');

// 书写循环语句,批量给元素添加监听
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
// 在这个函数中,this表示点击的这个元素,this涉及函数上下文的相关知识,我们在“面向对象”课程中介绍
this.style.color = 'red';
};
}
</script>
</body>

</html>

批量添加事件监听的性能问题

每一个事件监听注册都会消耗一定的系统内存,而批量添加事件会导致监听数量太多,内存消耗会非常大。

实际上,每个 <li> 的事件处理函数都是不同的函数,这些函数本身也会占用内存。

新增元素动态绑定事件

题目:页面上有一个无序列表 <ul>,它内部没有 <li> 元素,请制作一个按钮,点击这个按钮就能增加一个 <li> 元素。并且要求每个增加的 <li> 元素也要有点击事件监听,实现效果:点击哪个 <li> 元素,哪个 <li> 元素就变红。

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按我添加新的li列表项</button>
<ul id="list"></ul>

<script>
var oBtn = document.getElementById('btn');
var oList = document.getElementById('list');
var lis = oList.getElementsByTagName('li');

// 按钮的点击事件
oBtn.onclick = function () {
// 创建一个新的li列表项,孤儿节点
var oLi = document.createElement('li');
oLi.innerHTML = '我是列表项';
// 上树
oList.appendChild(oLi);
// 给新创建的这个li节点添加onclick事件监听
oLi.onclick = function () {
this.style.color = 'red';
};
};
// 注意:此处不能使用上一次循环的方式,因为在 var lis = oList.getElementsByTagName('li'); 的时候还没有得到任何东西,后面添加的 li 是监测不到的。
</script>
</body>

</html>

动态绑定事件的问题

新增元素必须分别添加事件监听,不能自动获得事件监听。

大量事件监听、大量事件处理函数都会产生大量的内存消耗。

事件委托

利用事件冒泡机制,将后代元素事件委托给祖先元素。

1736182016397.png

1736182017220.png

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的委托(delegation)。

e.target和e.currentTarget属性

事件委托通常需要结合使用 e.target 属性。

属性 属性描述
target 触发此事件的最早元素,即 “事件源元素”
currentTarget 事件处理程序附加到的元素
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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按我创建一个新列表项</button>
<ul id="list">
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
</ul>
<script>
var oList = document.getElementById('list');
var oBtn = document.getElementById('btn');

oList.onclick = function (e) {
// e.target表示用户真正点击的那个元素
e.target.style.color = 'red';
};

oBtn.onclick = function () {
// 创建新的li元素
var oLi = document.createElement('li');
// 写内容
oLi.innerText = '我是新来的';
// 上树
oList.appendChild(oLi);
};
</script>
</body>

</html>

style=“zoom: 67%;” />

事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意事件都有两个与事件相关的节点,一个是事件的原始触发节点(Event.target),另一个是事件当前正在通过的节点(Event.currentTarget)。前者通常是后者的后代节点。

Event.currentTarget属性返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。

Event.target属性返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。

事件传播过程中,不同节点的监听函数内部的Event.targetEvent.currentTarget属性的值是不一样的。

1
2
3
4
5
6
7
8
9
10
11
12
// HTML 代码为
// <p id="para">Hello <em>World</em></p>
function hide(e) {
// 不管点击 Hello 或 World,总是返回 true
console.log(this === e.currentTarget);

// 点击 Hello,返回 true
// 点击 World,返回 false
console.log(this === e.target);
}

document.getElementById('para').addEventListener('click', hide, false);

上面代码中,<em><p>的子节点,点击<em>或者点击<p>,都会导致监听函数执行。这时,e.target总是指向原始点击位置的那个节点,而e.currentTarget指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发,所以e.currentTarget总是等同于监听函数内部的this

使用事件委托时需要注意的事项

(1)onmouseenteronmouseover 都表示 “鼠标进入”,它们有什么区别呢?

答:onmouseenter 不冒泡,onmouseover 冒泡。

  • 使用事件委托时要注意:不能委托不冒泡的事件给祖先元素。

【onmouseenter】

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按我创建一个新列表项</button>
<ul id="list">
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
</ul>
<script>
var oList = document.getElementById('list');
var oBtn = document.getElementById('btn');

// onmouseenter这个属性天生就是“不冒泡”的,相当于你事件处理函数附加给了那个DOM节点
// 就是哪个DOM节点自己触发的事件,没有冒泡过程
// 再因为继承性,所以所有 li 会一起变色
oList.onmouseenter = function (e) {
// e.target表示用户真正点击的那个元素
e.target.style.color = 'red';
};

// oList.onmouseover = function (e) {
// // e.target表示用户真正点击的那个元素,即:那个 li
// e.target.style.color = 'red';
// };

oBtn.onclick = function () {
// 创建新的li元素
var oLi = document.createElement('li');
// 写内容
oLi.innerText = '我是新来的';
// 上树
oList.appendChild(oLi);
};
</script>
</body>

</html>

style=“zoom:67%;” />

【onmouseover】

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按我创建一个新列表项</button>
<ul id="list">
<li>列表项</li>
<li>列表项</li>
<li>列表项</li>
</ul>
<script>
var oList = document.getElementById('list');
var oBtn = document.getElementById('btn');

// onmouseenter这个属性天生就是“不冒泡”的,相当于你事件处理函数附加给了那个DOM节点
// 就是哪个DOM节点自己触发的事件,没有冒泡过程
// 再因为继承性,所以所有 li 会一起变色
// oList.onmouseenter = function (e) {
// // e.target表示用户真正点击的那个元素
// e.target.style.color = 'red';
// };

oList.onmouseover = function (e) {
// e.target表示用户真正点击的那个元素,即:那个 li
e.target.style.color = 'red';
};

oBtn.onclick = function () {
// 创建新的li元素
var oLi = document.createElement('li');
// 写内容
oLi.innerText = '我是新来的';
// 上树
oList.appendChild(oLi);
};
</script>
</body>

</html>

style=“zoom:67%;” />

(2)最内层元素不能再有额外的内层元素了,比如:

1736182017783.png

这会导致点击 li 时效果正常,但是点击 span 时,只有 span 会变色。

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按我创建一个新列表项</button>
<ul id="list">
<li><span>我是span</span>列表项</li>
<li><span>我是span</span>列表项</li>
<li><span>我是span</span>列表项</li>
<li><span>我是span</span>列表项</li>
<li><span>我是span</span>列表项</li>
</ul>
<script>
var oList = document.getElementById('list');
var oBtn = document.getElementById('btn');

oList.onmouseover = function (e) {
// e.target表示用户真正点击的那个元素,即:那个 li
e.target.style.color = 'red';
};

oBtn.onclick = function () {
// 创建新的li元素
var oLi = document.createElement('li');
// 写内容
oLi.innerText = '我是新来的';
// 上树
oList.appendChild(oLi);
};
</script>
</body>

</html>

style=“zoom:67%;” />

事件流

小案例引入

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
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box1 {
width: 202px;
height: 202px;
border: 1px solid #000;
padding: 50px;
}

#box2 {
width: 100px;
height: 100px;
border: 1px solid #000;
padding: 50px;
}

#box3 {
width: 100px;
height: 100px;
border: 1px solid #000;
}
</style>
</head>

<body>
<div id="box1">
<div id="box2">
<div id="box3"></div>
</div>
</div>
<script>
var oBox1 = document.getElementById('box1');
var oBox2 = document.getElementById('box2');
var oBox3 = document.getElementById('box3');

oBox2.onclick = function () {
console.log('我是 box2 的 onclick');
};

oBox3.onclick = function () {
console.log('我是 box3 的 onclick');
};

oBox1.onclick = function () {
console.log('我是 box1 的 onclick');
};
</script>
</body>

</html>

style=“zoom:50%;” />

1736182018520.png

1736182019307.png

捕获阶段和冒泡阶段

1736182020391.png

1736182020991.png

一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  • 第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
  • 第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。

这种三阶段的传播模型,使得同一个事件会在多个节点上触发。

addEventListener()的第三个参数

onxxx写法只能监听冒泡阶段

1736182021662.png

addEventListener()方法

1
oBox.addEventListener('click', function(){}, true);

Event:事件

1736182022585.png

1
2
3
<div>
<p>点击</p>
</div>

上面代码中,<div>节点之中有一个<p>节点。

如果对这两个节点,都设置click事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对<p>点击,click事件会触发四次。

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
var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};

var div = document.querySelector('div');
var p = document.querySelector('p');

div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);

function callback(event) {
var tag = event.currentTarget.tagName;
var phase = phases[event.eventPhase];
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}

// 点击以后的结果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'

上面代码表示,click事件被触发了四次:<div>节点的捕获阶段和冒泡阶段各1次,<p>节点的目标阶段触发了2次。

  1. 捕获阶段:事件从<div><p>传播时,触发<div>click事件;
  2. 目标阶段:事件从<div>到达<p>时,触发<p>click事件;
  3. 冒泡阶段:事件从<p>传回<div>时,再次触发<div>click事件。

其中,<p>节点有两个监听函数(addEventListener方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为click事件触发一次。所以,<p>会在target阶段有两次输出。

注意,浏览器总是假定click事件的目标节点,就是点击位置嵌套最深的那个节点(本例是<div>节点里面的<p>节点)。所以,<p>节点的捕获阶段和冒泡阶段,都会显示为target阶段。

事件传播的最上层对象是window,接着依次是documenthtmldocument.documentElement)和bodydocument.body)。也就是说,上例的事件传播顺序,在捕获阶段依次为windowdocumenthtmlbodydivp,在冒泡阶段依次为pdivbodyhtmldocumentwindow

结论:

  1. addEventListener 第3个参数决定了事件是在捕获阶段触发还是在冒泡阶段触发
  2. addEventListener 第3个参数为 true 表示捕获阶段触发,false 表示冒泡阶段触发,默认值为 false
  3. 事件流只会在父子元素具有相同事件类型时才会产生影响
  4. 绝大部分场景都采用默认的冒泡模式(其中一个原因是早期 IE 不支持捕获)

removeEventListener()方法

当我们 addEventListener() 后,该监听事件就会一直生效,直到关闭页面或是移除该对应的监听!

removeEventListener() 方法用来移除监听事件(只能移除具名函数的监听,且方法名称后面不能带 ()

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
var body = document.querySelector('body'),
var clickTarget = document.getElementById('click-target'),
var mouseOverTarget = document.getElementById('mouse-over-target'),
var toggle = false;

// 具名函数
function makeBackgroundYellow() {
'use strict';

if (toggle) {
body.style.backgroundColor = 'white';
} else {
body.style.backgroundColor = 'yellow';
}

toggle = !toggle;
}

// 注册监听
clickTarget.addEventListener('click',
makeBackgroundYellow,
false
);

// 注册监听
mouseOverTarget.addEventListener('mouseover', function () {
'use strict';

// 移除监听
clickTarget.removeEventListener('click',
makeBackgroundYellow,
false
);
});

特别注意

addEventListener() 一但注册某个事件,那么这个事件是会一直生效的,就算是该注册事件写在某个函数中,那个函数调用已经结束了,但是该事件还是会存在!因为事件的注册是直接绑定到相应的元素上的,并且是异步的,除非页面被关闭,或者是移除该监听!

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">点击</button>
<script>
var btn = document.getElementById('btn');

function demo() {
console.log('btn');
}

function test() {
btn.addEventListener('click', demo, false);
return 'over';
}

console.log(test());
</script>
</body>

</html>

执行结果:
over
(点击按钮)
btn
三大家族scroll、offset、client 定时器和延时器