DOM 事件
上一节已经能把学员数组显示成列表。现在要加入搜索、班级筛选和重置:用户点击按钮、输入关键字或切换班级时,列表应随操作更新。浏览器会为这些操作产生事件,JavaScript 监听事件后执行对应功能。
本节课解决五个问题:
- 怎样使用
addEventListener()绑定事件。 - 怎样通过事件对象获取操作信息。
input、change、click、submit等事件怎样选择。- 怎样阻止表单刷新和链接跳转。
- 事件为什么会从子元素向父元素冒泡。
动态列表的事件委托放到后面的专门章节。本节先掌握事件基础。
前面的短示例沿用各小节给出的 HTML 和变量;同一元素的不同绑定写法用于比较,不要全部叠加。综合案例中的 HTML 和 JavaScript 则按顺序放在同一个文件中。
事件绑定与事件对象
点击事件
<button id="search-button" type="button">搜索</button>
<p id="message">等待操作</p>const searchButton = document.querySelector("#search-button");
const message = document.querySelector("#message");
searchButton.addEventListener("click", () => {
message.textContent = "开始搜索学员";
});addEventListener() 可以先简单理解为:当指定事件发生时,调用给定函数。
element.addEventListener("事件名称", 处理函数);点击按钮后,浏览器调用箭头函数,段落文字会从“等待操作”变为“开始搜索学员”。第一个参数是事件名称,第二个参数是收到事件后执行的函数。
事件名称不加 on。下面的处理函数用于比较两种事件名称的写法:
function handleSearch() {
message.textContent = "开始搜索学员";
}
searchButton.addEventListener("click", handleSearch); // 正确
searchButton.addEventListener("onclick", handleSearch); // 不会监听到 click事件绑定方式
页面中可能看到三种写法。
HTML 行内事件
<button onclick="handleSearch()">搜索</button>事件和 HTML 混在一起,不利于维护,课程项目不使用。
DOM 事件属性
searchButton.onclick = handleSearch;同一个属性只能保留最后一个处理函数。下面两个函数分别输出不同文字,便于观察覆盖结果:
function firstHandler() {
console.log("更新学员列表");
}
function secondHandler() {
console.log("记录搜索操作");
}
searchButton.onclick = firstHandler;
searchButton.onclick = secondHandler; // 覆盖前一个addEventListener
searchButton.addEventListener("click", firstHandler);
searchButton.addEventListener("click", secondHandler);两个函数都会执行,也可以按函数引用单独移除。项目统一使用这种方式。
addEventListener() 让事件代码与 HTML 结构分开,也允许一个元素绑定多个职责。例如一个搜索按钮可以同时更新列表、记录操作日志;后面移除监听器时也能精确指定要移除哪个函数。
绑定时机
如果脚本没有使用 defer,也没有放在 </body> 前面,可以等待 DOM 创建完成:
document.addEventListener("DOMContentLoaded", () => {
const searchButton = document.querySelector("#search-button");
console.log(searchButton);
});当前课程项目优先使用外部脚本加 defer,不需要每个文件都重复包一层 DOMContentLoaded。
<script src="./app.js" defer></script>window 的 load 会等待图片等资源加载完成,通常比 DOMContentLoaded 更晚。普通 DOM 初始化不需要等待所有图片。
事件对象
浏览器调用处理函数时,会传入一个事件对象。
searchButton.addEventListener("click", (event) => {
console.log(event);
console.log(event.type);
console.log(event.target);
console.log(event.currentTarget);
});常用成员:
| 成员 | 作用 |
|---|---|
type | 当前事件名称 |
target | 最初触发事件的元素 |
currentTarget | 当前正在执行监听器的元素 |
key | 键盘按键名称 |
clientX、clientY | 鼠标在浏览器可视区域中的坐标 |
preventDefault() | 阻止浏览器默认行为 |
stopPropagation() | 阻止事件继续传播 |
参数名可以自己定义,但项目中通常使用 event:
searchButton.addEventListener("click", (event) => {
console.log(event.type);
});target 与 currentTarget
按钮内部可能还有图标:
<button id="save-button" type="button">
<span class="icon">✓</span>
保存
</button>const saveButton = document.querySelector("#save-button");
saveButton.addEventListener("click", (event) => {
console.log(event.target);
console.log(event.currentTarget);
});点击文字时,target 可能是按钮;点击图标时,target 可能是 span。currentTarget 始终是绑定监听器的按钮。
在普通按钮事件中,需要操作按钮本身时使用 currentTarget:
saveButton.addEventListener("click", (event) => {
event.currentTarget.disabled = true;
});点击内部图标时,target 可能是图标,currentTarget 仍然是按钮。普通按钮事件中要修改绑定监听器的按钮,优先使用 currentTarget。
事件目标辨析
打开下面的练习,分别点击“保存”文字和左侧图标,观察输出。附件把文字和图标分别放在两个 span 中:
event.target会随着实际点击位置变化:点击图标时是图标span,点击文字时是文字span。event.currentTarget始终是绑定监听器的button。
这一步只观察两个属性。下一节先把事件用于表单取值与校验;p04c07 再使用 closest(),从变化的 target 向上找到真正的操作按钮。
辨析 target 与 currentTarget 源码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>辨析 target 与 currentTarget</title>
<style>
body {
max-width: 680px;
margin: 40px auto;
padding: 0 20px;
color: #303133;
font-family: Arial, "Microsoft YaHei", sans-serif;
}
button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 18px;
border: 0;
border-radius: 6px;
background: #409eff;
color: #fff;
cursor: pointer;
font: inherit;
}
.button-icon {
display: inline-grid;
width: 24px;
height: 24px;
place-items: center;
border-radius: 4px;
background: rgb(255 255 255 / 22%);
}
dl {
display: grid;
grid-template-columns: 180px 1fr;
gap: 10px 16px;
margin-top: 24px;
padding: 18px;
background: #f5f7fa;
}
dt { font-weight: 700; }
dd { margin: 0; color: #606266; }
</style>
</head>
<body>
<h1>谁触发,谁在监听</h1>
<p>请分别点击按钮文字和左侧图标,比较两个事件属性。</p>
<button id="save-button" type="button">
<span class="button-icon" aria-hidden="true">✓</span>
<span class="button-label">保存</span>
</button>
<dl>
<dt>event.target</dt>
<dd id="target-result">等待点击</dd>
<dt>event.currentTarget</dt>
<dd id="current-target-result">等待点击</dd>
</dl>
<script>
const saveButton = document.querySelector("#save-button");
const targetResult = document.querySelector("#target-result");
const currentTargetResult = document.querySelector("#current-target-result");
function describeElement(element) {
const className = element.className;
return `<${element.tagName.toLowerCase()}>${className ? `.${className}` : ""}`;
}
saveButton.addEventListener("click", (event) => {
targetResult.textContent = describeElement(event.target);
currentTargetResult.textContent = describeElement(event.currentTarget);
});
</script>
</body>
</html>:::
常用事件与默认行为
鼠标事件
| 事件 | 触发时机 |
|---|---|
click | 单击 |
dblclick | 双击 |
mousedown | 按下鼠标按钮 |
mouseup | 松开鼠标按钮 |
mouseenter | 鼠标进入元素 |
mouseleave | 鼠标离开元素 |
mouseover | 进入元素或内部子元素,会冒泡 |
mouseout | 离开元素或内部子元素,会冒泡 |
普通悬停效果优先使用 CSS :hover。只有悬停后需要执行 JavaScript 逻辑时,才监听鼠标事件。
const studentCard = document.querySelector(".student-card");
studentCard.addEventListener("mouseenter", () => {
studentCard.classList.add("is-hover");
});
studentCard.addEventListener("mouseleave", () => {
studentCard.classList.remove("is-hover");
});mouseenter 和 mouseleave 不会因为鼠标在内部子元素之间移动而反复触发,更适合普通卡片进入和离开场景。
选择原则:单击按钮使用 click;普通视觉悬停优先写 CSS :hover;确实要在进入或离开卡片时执行 JavaScript,再使用 mouseenter、mouseleave。
输入、选择与焦点事件
表单输入最常用的是 input 和 change。
input
输入框内容每次发生变化时触发,适合实时字数、搜索建议和即时校验。
<label for="keyword">搜索关键字</label>
<input id="keyword" placeholder="请输入关键字" />
<span id="keyword-count">0 个字符</span>const keywordInput = document.querySelector("#keyword");
const keywordCount = document.querySelector("#keyword-count");
keywordInput.addEventListener("input", () => {
const length = keywordInput.value.length;
keywordCount.textContent = `${length} 个字符`;
});每输入或删除一个字符,input 都会触发一次,所以右侧字数会实时更新。
这里指用户编辑控件。JavaScript 执行 keywordInput.value = "张三" 不会自动触发 input 或 change;程序回填后如果需要重新统计或搜索,应显式调用对应的处理函数。
change
选择框、单复选框的值确定改变时触发;文本框通常在修改后失去焦点时触发。
<label for="class-name">班级</label>
<select id="class-name">
<option value="">全部班级</option>
<option value="Python 1 班">Python 1 班</option>
<option value="Python 2 班">Python 2 班</option>
</select>const classSelect = document.querySelector("#class-name");
classSelect.addEventListener("change", () => {
console.log(`当前班级:${classSelect.value || "全部班级"}`);
});用户切换下拉选项后,控制台会输出当前班级。文本框的 change 通常在用户修改后失去焦点时触发,因此实时搜索和实时字数不使用它。
| 需求 | 推荐事件 |
|---|---|
| 输入时实时显示字数 | input |
| 输入时显示搜索建议 | input |
| 下拉选项切换 | change |
| 复选框状态切换 | change |
| 用户完成文本修改后处理 | change |
focus 与 blur
| 事件 | 触发时机 |
|---|---|
focus | 控件获得焦点 |
blur | 控件失去焦点 |
keywordInput.addEventListener("focus", () => {
keywordInput.classList.add("is-focus");
});
keywordInput.addEventListener("blur", () => {
keywordInput.classList.remove("is-focus");
});很多焦点样式可以直接使用 CSS :focus。JavaScript 焦点事件适合失去焦点校验、记录当前编辑字段等逻辑。
JavaScript 也可以主动切换焦点:
keywordInput.focus();
keywordInput.blur();键盘事件
| 事件 | 触发时机 |
|---|---|
keydown | 按键按下,可以连续触发 |
keyup | 按键松开 |
通过 event.key 判断按键:
keywordInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
console.log("开始搜索");
}
if (event.key === "Escape") {
keywordInput.value = "";
keywordCount.textContent = "0 个字符";
}
});常见 key 值:
| 按键 | event.key |
|---|---|
| 回车 | Enter |
| Esc | Escape |
| 空格 | 一个空格字符串 |
| 方向键 | ArrowUp、ArrowDown、ArrowLeft、ArrowRight |
按下回车时,上面的代码会输出“开始搜索”;按下 Esc 时会清空输入框和字符数。程序修改 value 不会自动触发 input,所以要同步更新字符数。判断按键时使用 event.key,不要使用已经过时的 keyCode 数字。
表单提交与默认行为
使用 JavaScript 调 API 时,应监听表单的 submit,而不是只监听提交按钮的 click。
<form id="search-form">
<label for="search-keyword">搜索关键字</label>
<input id="search-keyword" name="keyword" />
<button type="submit">搜索</button>
</form>const searchForm = document.querySelector("#search-form");
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
const keyword = searchForm.elements.keyword.value.trim();
console.log(`搜索关键字:${keyword}`);
});这样点击按钮和在输入框中按回车,都会执行同一段搜索逻辑。
默认行为
浏览器对部分元素有默认行为:
- 提交表单会发送请求或刷新页面。
- 点击链接会跳转。
- 某些按键会滚动页面或提交表单。
preventDefault() 只阻止默认行为,不会停止事件传播。
<a id="course-link" href="./course.html">查看课程</a>const courseLink = document.querySelector("#course-link");
courseLink.addEventListener("click", (event) => {
event.preventDefault();
console.log("暂时不跳转,先检查权限");
});原生 addEventListener() 中只写 return false 不会阻止默认行为。下面是独立对比,测试时不要同时保留前面的 preventDefault() 监听器:
searchForm.addEventListener("submit", () => {
return false; // 不能代替 preventDefault()
});选择原则:按钮的局部操作使用 click;表单提交统一监听 submit;需要阻止刷新或跳转时,调用 event.preventDefault(),不要依赖 return false。
事件传播与监听器
事件冒泡
一次点击通常经历三个阶段:捕获阶段从外层祖先传向目标,目标阶段到达实际点击元素,冒泡阶段再从目标向祖先传播。addEventListener() 默认不监听捕获阶段;本节的父元素监听器通过冒泡收到点击,暂不使用 capture: true。
点击子元素时,事件会从最初元素向外层祖先元素传播,这个过程叫事件冒泡。
<article id="student-card">
<button id="edit-button" type="button">编辑</button>
</article>const studentCard = document.querySelector("#student-card");
const editButton = document.querySelector("#edit-button");
studentCard.addEventListener("click", () => {
console.log("点击了学员卡片");
});
editButton.addEventListener("click", () => {
console.log("点击了编辑按钮");
});点击编辑按钮后,通常依次输出:
点击了编辑按钮
点击了学员卡片传播过程可以简单表示为:
冒泡不是错误。后面的事件委托章节正是利用这个机制处理动态列表。
并非所有事件都会冒泡。常用的 click、input、change 可以按这个思路处理;focus、blur、mouseenter、mouseleave 不冒泡。需要委托焦点变化时,可以使用会冒泡的 focusin、focusout。
阻止冒泡
确实不希望父元素响应时,可以调用:
editButton.addEventListener("click", (event) => {
event.stopPropagation();
console.log("只执行编辑操作");
});不要看到重复执行就随意添加 stopPropagation()。先检查父子元素的职责,确认传播确实不需要再阻止。
stopPropagation() 和 preventDefault() 解决的问题不同:前者阻止事件继续向外传播,后者阻止浏览器默认动作。提交表单时通常需要后者,点击卡片内独立按钮时才可能需要前者。
监听器移除与单次执行
removeEventListener() 必须使用绑定时的同一个函数引用。
function handleSearch() {
console.log("搜索学员");
}
searchButton.addEventListener("click", handleSearch);
searchButton.removeEventListener("click", handleSearch);下面的写法无法移除:
searchButton.addEventListener("click", () => {
console.log("搜索学员");
});
searchButton.removeEventListener("click", () => {
console.log("搜索学员");
});两个箭头函数看起来一样,但它们是两个不同的函数对象。
有些引导提示只需要响应一次:
<button id="help-button" type="button">帮助</button>const helpButton = document.querySelector("#help-button");
helpButton.addEventListener(
"click",
() => {
console.log("显示首次使用提示");
},
{ once: true },
);once: true 表示执行一次后由浏览器自动移除监听器。
综合案例:学员条件搜索
根据本地学员数组完成条件搜索:
- 输入关键字时实时显示字符数。
- 提交搜索表单时阻止页面刷新。
- 根据姓名、手机号和班级过滤学员。
- 切换班级时自动重新搜索。
- 在关键字输入框按
Escape清空条件。 - 点击重置按钮恢复全部学员。
下面的 HTML 放在 <body> 中,后续 JavaScript 代码按顺序放在 </body> 前的同一个 <script> 中。列表节点的创建沿用上一节的方法,这里一并写出,方便单独复现本案例。
页面结构
<main>
<h1>学员条件搜索</h1>
<form id="search-form">
<label for="keyword">搜索关键字</label>
<input id="keyword" name="keyword" type="search" />
<span id="keyword-count">0 个字符</span>
<label for="class-name">班级</label>
<select id="class-name" name="class_name">
<option value="">全部班级</option>
<option value="Python 1 班">Python 1 班</option>
<option value="Python 2 班">Python 2 班</option>
<option value="Web 全栈班">Web 全栈班</option>
</select>
<button type="submit">搜索</button>
<button id="reset-button" type="button">重置</button>
</form>
<h2>搜索结果</h2>
<span id="result-summary">正在准备数据...</span>
<section id="student-list"></section>
</main>学员数据与列表渲染
准备 6 名本地学员,并查询页面中需要更新的元素:
const students = [
{ id: 1, name: "张三", className: "Python 1 班", mobile: "13800000001" },
{ id: 2, name: "李四", className: "Python 2 班", mobile: "13800000002" },
{ id: 3, name: "王五", className: "Web 全栈班", mobile: "13800000003" },
{ id: 4, name: "赵六", className: "Python 1 班", mobile: "13800000004" },
{ id: 5, name: "钱七", className: "Python 2 班", mobile: "13800000005" },
{ id: 6, name: "孙八", className: "Web 全栈班", mobile: "13800000006" },
];
const searchForm = document.querySelector("#search-form");
const keywordInput = document.querySelector("#keyword");
const keywordCount = document.querySelector("#keyword-count");
const classSelect = document.querySelector("#class-name");
const resetButton = document.querySelector("#reset-button");
const studentList = document.querySelector("#student-list");
const resultSummary = document.querySelector("#result-summary");用学员数据创建卡片,再根据结果数组更新列表和统计文字:
function createStudentCard(student) {
const card = document.createElement("article");
const name = document.createElement("h3");
const className = document.createElement("p");
const mobile = document.createElement("p");
card.classList.add("student-card");
card.dataset.id = String(student.id);
name.textContent = student.name;
className.textContent = `班级:${student.className}`;
mobile.textContent = `手机号:${student.mobile}`;
card.append(name, className, mobile);
return card;
}
function renderStudentList(studentData) {
if (studentData.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.classList.add("empty-message");
emptyMessage.textContent = "没有找到符合条件的学员";
studentList.replaceChildren(emptyMessage);
resultSummary.textContent = "共 0 条结果";
return;
}
const fragment = document.createDocumentFragment();
for (const student of studentData) {
fragment.append(createStudentCard(student));
}
studentList.replaceChildren(fragment);
resultSummary.textContent = `共 ${studentData.length} 条结果`;
}studentList 使用 <section>,因此既能放学员卡片,也能在空结果时放提示段落。列表渲染函数会替换旧节点,避免多次搜索后出现重复卡片。
条件过滤
function filterStudents(students, keyword, className) {
const normalizedKeyword = keyword.trim().toLowerCase();
return students.filter((student) => {
const matchesKeyword =
!normalizedKeyword ||
student.name.toLowerCase().includes(normalizedKeyword) ||
student.mobile.includes(normalizedKeyword);
const matchesClass =
!className || student.className === className;
return matchesKeyword && matchesClass;
});
}空关键字和空班级都表示“不限制这一项条件”。只有姓名或手机号匹配关键字,并且班级也匹配时,学员才会进入结果数组。
搜索与事件绑定
function runSearch() {
const keyword = keywordInput.value;
const className = classSelect.value;
const result = filterStudents(students, keyword, className);
renderStudentList(result);
}搜索函数主动读取当前表单状态。提交、班级切换和清空条件都可以调用它。
绑定事件
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
runSearch();
});
keywordInput.addEventListener("input", () => {
const length = keywordInput.value.length;
keywordCount.textContent = `${length} 个字符`;
});
classSelect.addEventListener("change", runSearch);
keywordInput.addEventListener("keydown", (event) => {
if (event.key !== "Escape") {
return;
}
keywordInput.value = "";
keywordCount.textContent = "0 个字符";
runSearch();
});
resetButton.addEventListener("click", () => {
searchForm.reset();
keywordCount.textContent = "0 个字符";
runSearch();
keywordInput.focus();
});
renderStudentList(students);同一个 runSearch() 被提交、班级切换、Esc 清空和重置复用,避免把过滤逻辑复制到每个监听器中。最后先渲染全部 6 名学员,等待用户操作。
运行与验收
打开学员条件搜索(源码文件:./.code/p04/c05-events/04-student-search.html),默认应显示 6 名学员和“共 6 条结果”。
可以按下面步骤验证:
- 输入“张”,右侧字符数应实时变化;点击搜索或按回车后,只显示张三。
- 选择“Python 2 班”,列表应只显示李四和钱七;再输入一个不存在的关键字,应显示空数据提示。
- 在关键字输入框按 Esc,应清空关键字并恢复当前班级下的结果。
- 点击重置,应恢复全部学员,焦点回到关键字输入框。
完整附件还包含页面样式:
学员条件搜索
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DOM 事件:学员条件搜索</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #f5f7fa;
color: #303133;
font-family: Arial, "Microsoft YaHei", sans-serif;
}
.page-container {
width: min(980px, calc(100% - 32px));
margin: 48px auto;
}
.page-title {
margin: 0 0 22px;
font-size: 28px;
}
.search-panel {
margin-bottom: 18px;
padding: 20px;
border-radius: 8px;
background: #fff;
box-shadow: 0 5px 18px rgb(0 0 0 / 5%);
}
.search-form {
display: grid;
grid-template-columns: minmax(0, 1fr) 220px auto;
gap: 12px;
align-items: end;
}
.search-form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
.input-wrap {
position: relative;
}
input,
select,
button {
min-height: 42px;
border-radius: 5px;
font: inherit;
}
input,
select {
width: 100%;
border: 1px solid #dcdfe6;
background: #fff;
outline: none;
}
input {
padding: 9px 92px 9px 12px;
}
select {
padding: 9px 12px;
}
input:focus,
select:focus {
border-color: #16baaa;
}
#keyword-count {
position: absolute;
top: 40px;
right: 12px;
color: #909399;
font-size: 13px;
}
.button-group {
display: flex;
gap: 8px;
}
button {
padding: 0 18px;
border: 1px solid transparent;
cursor: pointer;
}
button[type="submit"] {
background: #16baaa;
color: #fff;
}
#reset-button {
border-color: #dcdfe6;
background: #fff;
color: #606266;
}
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 22px 0 14px;
}
.result-header h2 {
margin: 0;
font-size: 20px;
}
#result-summary {
color: #606266;
font-size: 14px;
}
.student-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.student-card {
padding: 20px;
border-radius: 8px;
background: #fff;
box-shadow: 0 4px 15px rgb(0 0 0 / 5%);
}
.student-card h3 {
margin: 0 0 12px;
}
.student-card p {
margin: 7px 0;
color: #606266;
}
.empty-message {
grid-column: 1 / -1;
padding: 70px 20px;
border-radius: 8px;
background: #fff;
color: #909399;
text-align: center;
}
@media (max-width: 760px) {
.search-form,
.student-list {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main class="page-container">
<h1 class="page-title">学员条件搜索</h1>
<section class="search-panel">
<form id="search-form" class="search-form">
<div class="input-wrap">
<label for="keyword">搜索关键字</label>
<input
id="keyword"
name="keyword"
type="search"
autocomplete="off"
placeholder="搜索姓名或手机号,按 Esc 清空"
/>
<span id="keyword-count">0 个字符</span>
</div>
<div>
<label for="class-name">班级</label>
<select id="class-name" name="class_name">
<option value="">全部班级</option>
<option value="Python 1 班">Python 1 班</option>
<option value="Python 2 班">Python 2 班</option>
<option value="Web 全栈班">Web 全栈班</option>
</select>
</div>
<div class="button-group">
<button type="submit">搜索</button>
<button id="reset-button" type="button">重置</button>
</div>
</form>
</section>
<header class="result-header">
<h2>搜索结果</h2>
<span id="result-summary">正在准备数据...</span>
</header>
<section id="student-list" class="student-list"></section>
</main>
<script>
const students = [
{ id: 1, name: "张三", className: "Python 1 班", mobile: "13800000001" },
{ id: 2, name: "李四", className: "Python 2 班", mobile: "13800000002" },
{ id: 3, name: "王五", className: "Web 全栈班", mobile: "13800000003" },
{ id: 4, name: "赵六", className: "Python 1 班", mobile: "13800000004" },
{ id: 5, name: "钱七", className: "Python 2 班", mobile: "13800000005" },
{ id: 6, name: "孙八", className: "Web 全栈班", mobile: "13800000006" },
];
const searchForm = document.querySelector("#search-form");
const keywordInput = document.querySelector("#keyword");
const keywordCount = document.querySelector("#keyword-count");
const classSelect = document.querySelector("#class-name");
const resetButton = document.querySelector("#reset-button");
const studentList = document.querySelector("#student-list");
const resultSummary = document.querySelector("#result-summary");
function createStudentCard(student) {
const card = document.createElement("article");
const name = document.createElement("h3");
const className = document.createElement("p");
const mobile = document.createElement("p");
card.classList.add("student-card");
card.dataset.id = String(student.id);
name.textContent = student.name;
className.textContent = `班级:${student.className}`;
mobile.textContent = `手机号:${student.mobile}`;
card.append(name, className, mobile);
return card;
}
function renderStudentList(studentData) {
if (studentData.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.classList.add("empty-message");
emptyMessage.textContent = "没有找到符合条件的学员";
studentList.replaceChildren(emptyMessage);
resultSummary.textContent = "共 0 条结果";
return;
}
const fragment = document.createDocumentFragment();
for (const student of studentData) {
fragment.append(createStudentCard(student));
}
studentList.replaceChildren(fragment);
resultSummary.textContent = `共 ${studentData.length} 条结果`;
}
function filterStudents(students, keyword, className) {
const normalizedKeyword = keyword.trim().toLowerCase();
return students.filter((student) => {
const matchesKeyword =
!normalizedKeyword ||
student.name.toLowerCase().includes(normalizedKeyword) ||
student.mobile.includes(normalizedKeyword);
const matchesClass =
!className || student.className === className;
return matchesKeyword && matchesClass;
});
}
function runSearch() {
const keyword = keywordInput.value;
const className = classSelect.value;
const result = filterStudents(students, keyword, className);
renderStudentList(result);
}
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
runSearch();
});
keywordInput.addEventListener("input", () => {
const length = keywordInput.value.length;
keywordCount.textContent = `${length} 个字符`;
});
classSelect.addEventListener("change", runSearch);
keywordInput.addEventListener("keydown", (event) => {
if (event.key !== "Escape") {
return;
}
keywordInput.value = "";
keywordCount.textContent = "0 个字符";
runSearch();
});
resetButton.addEventListener("click", () => {
searchForm.reset();
keywordCount.textContent = "0 个字符";
runSearch();
keywordInput.focus();
});
renderStudentList(students);
</script>
</body>
</html>小结
本节课最核心的是掌握六件事:
- 使用
addEventListener()绑定事件,事件名称不加on。 - 绑定时传入函数引用,不要提前调用函数。
target表示最初触发元素,currentTarget表示当前监听元素。- 表单监听
submit,并用preventDefault()阻止默认刷新。 input适合实时变化,change适合选项或确定后的变化。- 点击事件默认向上冒泡,它是后续事件委托的基础。