# JavaScript教程 - 16 DOM
DOM 继续!
# 16.12 事件
重新来说回事件。
# 1 事件对象
什么是事件对象?
浏览器在事件触发时自动创建的一个特殊对象,包含了与该事件相关的所有信息和功能(包括鼠标、键盘等信息)。例如鼠标相关的事件,会包含事件触发时候的鼠标位置。
举个栗子:
鼠标在 div 中移动,获取事件对象。
<body>
<div id="div1" style="width: 100px; height: 100px; background-color: lightblue;"></div>
<script>
const div1 = document.getElementById('div1');
div1.onmousemove = function (event) {
div1.innerHTML = event.x + ", " + event.y; // 可以获取事件的详细信息
};
</script>
</body>
2
3
4
5
6
7
8
9
10
11
- 在上面的代码中,给 div 添加鼠标移动事件回调方法,回调方法的第一个参数就是事件对象。
- 我们可以从事件对象中获取到鼠标当前的位置,
event.x
和event.y
表示鼠标相对于浏览器窗口的坐标,event.x
和event.y
也可以使用clientX
和clientY
。
运行如下:
同样,通过箭头函数或者 addEventListener
事件监听的方式也可以获取。
div1.onmousemove = event => {
div1.innerHTML = event.x + ", " + event.y;
}
div1.addEventListener("mousemove", function () {
div1.innerHTML = event.x + ", " + event.y;
});
div1.addEventListener("mousemove", event => {
div1.innerHTML = event.x + ", " + event.y;
});
2
3
4
5
6
7
8
9
10
11
# 2 事件的常用属性
不同的事件,事件对象是不同的,但是它们都继承自 Event 对象。
<body>
<div id="div1" style="width: 100px; height: 100px; background-color: lightblue; margin: 100px;"></div>
<script>
const div1 = document.getElementById('div1');
div1.onclick = function (event) {
console.log(event.offsetX + ', ' + event.offsetY); // 鼠标相对于事件目标元素的坐标
console.log(event.x + ', ' + event.y); // 鼠标相对于浏览器窗口的坐标
console.log(event.screenX + ', ' + event.screenY); // 鼠标相对于屏幕的坐标
console.log(event.pageX + ', ' + event.pageY); // 鼠标相对于整个文档的坐标(包含滚动)
};
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
- 上面通过一些方式获取到了鼠标相对于不同对象的位置。
# 3 事件冒泡
什么是事件冒泡?
事件冒泡是指当 DOM 元素上的事件被触发时,这个事件会从该元素向上传播到它的父元素、祖先元素,直到 document
根节点。
例如,div1 > div2 > div3
三个 div 嵌套,当点击 div3
的时候,会触发 div3
的点击事件,然后点击事件会向上传播给 div2
,然后继续传播给 div1
。
演示一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>For技术栈</title>
<style>
#div1 {
width: 200px;
height: 200px;
background-color: lightblue;
}
#div2 {
width: 150px;
height: 150px;
background-color: lightgreen;
}
#div3 {
width: 100px;
height: 100px;
background-color: pink;
}
</style>
</head>
<body>
<div id="div1">
<div id="div2">
<div id="div3"></div>
</div>
</div>
<script>
const div1 = document.getElementById('div1');
const div2 = document.getElementById('div2');
const div3 = document.getElementById('div3');
div1.addEventListener('click', ) = function (event) {
alert('点击div1');
};
div2.onclick = function (event) {
alert('点击div2');
};
div3.onclick = function (event) {
alert('点击div3');
};
</script>
</body>
</html>
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
显示如下:
当点击 div3 的时候,会依次弹出 点击div3
、点击div2
、点击div1
,一次传递给父元素。
需要注意,事件的冒泡,和事件的订阅是没有关系的,div3没有订阅点击事件,事件也是会传递给 div2 和 div1 的。另外和 CSS 样式无关,div3 在 div2 中,即使通过 CSS 样式,让 div3 显示到 div2 的外面,div3 的样式还是会传递给 div2 和 div1,因为从 HTML 结构上它们是父子关系。
事件的冒泡对于我们的开发是有利的,会简化我们的开发。
当然,我们也可以在 div3 的点击事件中,取消点击事件的冒泡,这样 div2 和 div1 就收不到事件了。
举个例子:
div3.onclick = function (event) {
alert('点击div3');
event.stopPropagation(); // 停止事件冒泡
};
2
3
4
- 在 div3 的点击事件处理函数中,可以通过
event.stopPropagation();
停止事件的冒泡。
# 4 触发事件的对象
在事件的回调函数中,我们可以获取到触发事件的对象,还可以获取绑定事件的对象。
举个栗子:
如果事件是从 div3 传递给 div1 的,那么在 div1 的回调函数中
div1.onclick = function (event) {
console.log(event.target); // 触发事件的对象<div id="div3">
console.log(this); // 绑定事件的对象,<div id="div1">
console.log(event.currentTarget); // 绑定事件的对象,同this <div id="div1">
};
2
3
4
5
event.target
表示当前触发事件的对象,事件是 div3 触发的传递给 div1 的,那么这里 target 还是 div3。this
(非箭头函数)表示绑定事件的对象,这里是 div1,event.currentTarget
表示绑定事件的对象,所以和 this 相同,如果在 箭头函数中,没有办法获取到 this,那么可以使用event.currentTarget
获取到绑定事件的对象。- 如果在 div3 的事件回调函数中,那么
event.target
、this
、event.currentTarget
是相同的。
# 5 阻止默认行为
<a>
标签在点击的时候,会跳转到 href
属性指定的地址,在讲解 HTML5 的时候说过,可以通过如下方式阻止 <a>
标签的默认行为:
<a href="javascript:void(0);">点击我什么都不会发生</a>
<!-- 或者 -->
<a href="javascript:;">点击我什么都不会发生</a>
2
3
有时候我们需要通过监听 <a>
的点击事件,做一些处理,那么可以像上面这样禁用 <a>
标签的默认行为。
除了上面这种方式,我们还可以在 <a>
标签的点击事件中,通过 event.preventDefault();
阻止 <a>
标签的默认行为:
<a id="my-a" href="https://www.foooor.com">For技术栈</a>
<script>
const myA = document.getElementById('my-a');
myA.onclick = function(event) {
event.preventDefault(); // 阻止默认行为
alert('点击了a标签');
};
</script>
2
3
4
5
6
7
8
9
10
# 6 事件的委派
什么是事件的委派?
事件委派(Event Delegation)是 JavaScript 中的一种事件处理技巧,它利用事件冒泡,把多个子元素的事件,统一交给父元素来监听和处理,从而减少事件绑定数量、提高性能和灵活性。
举个栗子:
查看一下下面的两种事件绑定方式。
<body>
<ul id="list">
<li>苹果</li>
<li>香蕉</li>
<li>橘子</li>
</ul>
<script>
// 方式一:传统方式
const liItems = document.querySelectorAll("#list li");
// 遍历liItems,给每个li添加点击事件
liItems.forEach((li) => {
li.addEventListener("click", () => {
console.log("你点击了", li.textContent);
});
});
// 方式二:事件委派方式
const listEle = document.getElementById("list");
// 给ul添加点击事件
listEle.addEventListener("click", (e) => {
// 将liItems转换为数组,判断点击的是否是数组中的li
const liArray = Array.from(liItems);
if (liArray.includes(e.target)) {
console.log("你点击了", e.target.textContent);
}
});
</script>
</body>
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
- 在上面的代码中,方式一是给所有的元素都绑定事件;
- 方式二是将元素的绑定事件委托给父元素,当点击子元素的时候,会通过事件冒泡传递给父元素,然后在判断点击的是哪个子元素,这样就减少了事件的绑定数量。
如果我们此时向父元素中动态的添加子元素,通过第二种事件委派的方式,就不需要动态添加的元素添加绑定事件,从而简化了处理。
# 7 事件的传播机制
事件的传播其实是经历三个阶段的:
捕获阶段:事件从最外层(
window
/document
)开始,也就是事件一开始是由祖先元素捕获,然后从祖先元素依次向子元素传播,直到传播到目标元素。目标阶段:事件到达真正的目标元素,此阶段事件会在目标元素本身触发。
冒泡阶段:事件从目标元素开始,逐层向上传播回祖先元素,直到
document
。
也就是事件是由祖先元素捕获,然后传播给目标元素,然后再由目标元素逐层向上传播回祖先元素。
我们在监听事件进行处理的时候,默认是在冒泡阶段触发的,可以通过监听事件的第三个参数,将触发阶段修改到捕获阶段。
举个栗子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>For技术栈</title>
<style>
#div1 {
width: 200px;
height: 200px;
background-color: lightblue;
}
#div2 {
width: 150px;
height: 150px;
background-color: lightgreen;
}
#div3 {
width: 100px;
height: 100px;
background-color: pink;
}
</style>
</head>
<body>
<div id="div1">
<div id="div2">
<div id="div3"></div>
</div>
</div>
<script>
const div1 = document.getElementById('div1');
const div2 = document.getElementById('div2');
const div3 = document.getElementById('div3');
div1.addEventListener('click', function (event) {
alert('点击div1');
}, true);
div2.addEventListener('click', function (event) {
alert('点击div2');
}, true);
div3.addEventListener('click', function (event) {
alert('点击div3');
}, true);
</script>
</body>
</html>
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
- 上面通过设置
addEventListener
第三个参数为 true,让事件在捕获的阶段触发,这样在点击 div3 的时候,会依次弹出点击div1 > 点击div2 > 点击div3
。 - 在捕获阶段,也同样可以通过
event.stopPropagation();
方法阻止事件的传播。
一般情况下,我们是不需要修改的,除非有特殊需求。
# 8 常用事件
# 鼠标事件
可绑定几乎所有可视元素,如按钮、图片、div、a 标签等。
事件名 | 描述 | 示例代码 |
---|---|---|
click | 单击(左键) | button.addEventListener("click", fn) |
dblclick | 双击 | div.addEventListener("dblclick", fn) |
mousedown | 鼠标按下 | div.addEventListener("mousedown", fn) |
mouseup | 鼠标释放 | |
mousemove | 鼠标移动 | 用于绘图、拖动 |
mouseenter | 鼠标进入(不冒泡) | 类似 mouseover ,但不涉及子元素 |
mouseleave | 鼠标离开(不冒泡) | |
mouseover | 鼠标移入(会冒泡) | 用于提示、浮层 |
mouseout | 鼠标移出(会冒泡) | |
contextmenu | 鼠标右键 | 可自定义右键菜单 |
举个栗子:
document.querySelector("button").addEventListener("click", () => {
alert("按钮被点击了");
});
2
3
# 键盘事件
默认可以绑定可以获取到焦点的元素,例如<input>
、<textarea>
、<button>
和 document
或 window
对象。如果是非表单元素,例如 div
,需添加 tabindex="0"
属性才可以获得焦点,然后可以接收键盘事件。
事件名 | 描述 | 注意事项 |
---|---|---|
keydown | 按键按下(包括功能键) | 最常用,支持组合键 |
keyup | 按键释放 | 可用于监听输入完成 |
举个栗子:
document.addEventListener("keydown", (e) => {
console.log(e.key + ", " + e.keyCode); // 按下按键会打印按键名字和按键的号码
});
document.addEventListener("keydown", (e) => {
if (e.ctrlKey && e.key === "s") { // 同时按下ctrl+s
e.preventDefault();
alert("保存命令被拦截");
}
});
2
3
4
5
6
7
8
9
10
# 表单事件
可绑定表单类元素,如 <form>
、<input>
、<textarea>
、<select>
事件名 | 描述 | 注意事项 |
---|---|---|
submit | 表单提交 | 必须阻止默认提交:e.preventDefault() |
reset | 表单重置 | |
change | 内容改变(失去焦点时才触发) | 比如输入框输入后点到别处才触发 |
input | 实时输入(每输入一次就触发一次) | 用于动态校验、计数 |
focus | 获取焦点 | 不会冒泡 |
blur | 失去焦点 | 不会冒泡 |
举个栗子:
阻止表单提交:
document.querySelector("form").addEventListener("submit", function (e) {
e.preventDefault();
alert("提交被拦截");
});
2
3
4
# 窗口与文档事件
可绑定元素:window
、document
事件名 | 描述 |
---|---|
load | 页面资源(包括图片)全部加载完 |
DOMContentLoaded | 仅 DOM 结构加载完成(推荐) |
resize | 浏览器窗口大小变化 |
scroll | 页面或元素滚动 |
举个例子:
window.addEventListener("scroll", function (e) {
console.log("滚动")
});
2
3
# 剪贴板事件
可绑定任何可编辑元素,如 input
、textarea
,或使用 contenteditable="true"
的元素
事件名 | 描述 |
---|---|
copy | 复制时触发 |
cut | 剪切时触发 |
paste | 粘贴时触发 |
举个栗子:
禁止粘贴:
input.addEventListener("paste", (e) => {
e.preventDefault();
alert("禁止粘贴!");
});
2
3
4
下面来做两个练习。
# 16.13 学生信息表
实现一个能添加和删除学生信息的表格,显示如下:
实现思路:
给添加按钮绑定点击事件,点击按钮的时候,获取到姓名和年龄,然后创建 <tr>
元素添加到表格的 <tbody>
中即可。 <tr>
元素中需要创建三个 <td>
,在第三个 <td>
中添加删除按钮,并绑定点击事件,用于删除 <tr>
。
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>学生信息表</title>
<style>
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
table {
margin-top: 10px;
min-width: 600px;
}
th, td {
padding: 5px 10px;
}
</style>
</head>
<body>
<h2>学生信息表</h2>
姓名:<input type="text" id="name" />
年龄:<input type="number" id="age" />
<button id="addBtn">添加</button>
<table id="studentTable">
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- 动态插入的学生行在这里 -->
</tbody>
</table>
<script>
// 名字输入框
const nameInput = document.getElementById("name");
// 年龄输入框
const ageInput = document.getElementById("age");
// 添加按钮
const addBtn = document.getElementById("addBtn");
// 表格的body
const tbody = document.querySelector("#studentTable tbody");
// 给添加按钮绑定点击事件
addBtn.addEventListener("click", function () {
// 获取到输入框的内容,并去掉前后的空格
const name = nameInput.value.trim();
const age = ageInput.value.trim();
if (!name || !age) {
alert("请输入完整信息!");
return;
}
// 创建一行tr
const tr = document.createElement("tr");
// 创建姓名单元格
const tdName = document.createElement("td");
tdName.textContent = name;
// 创建年龄单元格
const tdAge = document.createElement("td");
tdAge.textContent = age;
// 创建操作单元格和删除按钮
const tdOp = document.createElement("td");
const delBtn = document.createElement("button");
delBtn.textContent = "删除";
// 给按钮添加绑定事件
delBtn.addEventListener("click", () => {
tbody.removeChild(tr); // 删除tr
});
tdOp.appendChild(delBtn);
// 添加单元格到行中
tr.appendChild(tdName);
tr.appendChild(tdAge);
tr.appendChild(tdOp);
// 添加行到表格中
tbody.appendChild(tr);
// 清空输入框
nameInput.value = "";
ageInput.value = "";
});
</script>
</body>
</html>
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# 16.14 拖拽
创建一个 div,在页面上可以随意拖拽。
如下图:
实现思路:
当鼠标按下时,记录鼠标相对于元素的偏移量,在移动的时候,设置鼠标也是在元素的相同位置;
鼠标移动,元素跟随鼠标移动;
鼠标抬起时,完成拖拽。
代码如下:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>拖拽练习</title>
<style>
#drag-box {
width: 100px;
height: 100px;
background-color: lightblue;
position: absolute; /* 关键 */
top: 100px;
left: 100px;
}
</style>
</head>
<body>
<div id="drag-box">拖我</div>
<script>
const box = document.getElementById('drag-box');
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
box.addEventListener('mousedown', function (e) {
isDragging = true;
// 计算鼠标点击位置相对 div 的偏移,用于拖拽的准确位置
offsetX = e.clientX - box.offsetLeft;
offsetY = e.clientY - box.offsetTop;
// 防止选中文字
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (isDragging) {
// 实时修改div的位置
box.style.left = (e.clientX - offsetX) + 'px';
box.style.top = (e.clientY - offsetY) + 'px';
}
});
document.addEventListener('mouseup', function () {
isDragging = false;
});
</script>
</body>
</html>
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