DOM 与事件
这一课你将能做到
- 用
querySelector / getElementById 找到页面元素并改文本
- 用
addEventListener 监听点击与表单提交,并正确使用 preventDefault
- 解释事件委托的直观好处(父元素统一听),能读懂
closest 示例
为什么要学
控制台算数很爽,但用户点的是页面。
DOM(Document Object Model)人话:浏览器把 HTML 变成一棵可操作的树;JS 通过它改页面。
事件人话:点击、输入、提交时浏览器发出通知,你的函数去接。
不会 DOM,就无法把变量/数组/函数接到真实界面上。
后面的 React 会帮你「声明式」更新界面,但底层仍是这些概念。
先建立图像
HTML 标签 → 浏览器建成 DOM 树 → JS 找到节点改它
点击按钮 → 触发 click 事件 → 你绑定的函数运行
document
└─ html
├─ head
└─ body
├─ button#btn
└─ p#out
| API | 人话 |
|---|
getElementById | 按 id 精确找一个 |
querySelector | 按 CSS 选择器找第一个 |
querySelectorAll | 找一堆,再循环 |
addEventListener | 订阅某种事件 |
textContent | 改纯文本(优先) |
跟着做
步骤 1:查找与修改
<p id="tip">原始文字</p>
<script>
// CSS 选择器语法:#tip 表示 id=tip
const tip = document.querySelector("#tip");
tip.textContent = "被 JS 改过了";
</script>
优先用 textContent 改文本;少用 innerHTML 拼用户输入(有 XSS 风险:恶意脚本被插入页面)。
步骤 2:监听点击,维护计数状态
<button type="button" id="btn">+1</button>
<span id="n">0</span>
<script>
let count = 0;
const n = document.getElementById("n");
document.getElementById("btn").addEventListener("click", function () {
count = count + 1; // 先改数据
n.textContent = String(count); // 再更新界面
});
</script>
你应看到每次点击数字加一。模式仍是:状态在变量里,DOM 是投影。
步骤 3:表单提交不要整页刷新
<form id="f">
<label>留言 <input name="msg" required /></label>
<button type="submit">发送</button>
</form>
<p id="out"></p>
<script>
document.getElementById("f").addEventListener("submit", function (e) {
e.preventDefault(); // 阻止默认「提交刷新」
const fd = new FormData(e.target);
const msg = String(fd.get("msg") || "").trim();
const out = document.getElementById("out");
if (!msg) {
out.textContent = "留言不能为空";
return;
}
out.textContent = "你说:" + msg;
});
</script>
不写 preventDefault 时,浏览器可能整页刷新,你的 JS 状态会丢光。
步骤 4:事件委托(思想)
<ul id="list">
<li><button type="button" data-id="1">任务一</button></li>
<li><button type="button" data-id="2">任务二</button></li>
</ul>
<script>
document.getElementById("list").addEventListener("click", function (e) {
// 从实际点击目标往上找带 data-id 的 button
const btn = e.target.closest("button[data-id]");
if (!btn) return; // 点到空白处就忽略
console.log("点了", btn.dataset.id);
});
</script>
步骤 5:创建节点并挂到树上
const ul = document.getElementById("list");
const li = document.createElement("li");
const btn = document.createElement("button");
btn.type = "button";
btn.dataset.id = "3";
btn.textContent = "任务三";
li.appendChild(btn);
ul.appendChild(li);
你应在列表末尾看到新按钮;步骤 4 的委托监听无需重绑也能响应新按钮——这是委托的重要好处。
步骤 6:完整小页——待办一瞥
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DOM 与事件</title>
</head>
<body>
<input id="title" type="text" placeholder="新任务" />
<button type="button" id="add">添加</button>
<ul id="list"></ul>
<script>
const items = []; // { id, title }
function render() {
const list = document.getElementById("list");
list.replaceChildren(); // 清空再画(现代 API)
for (const item of items) {
const li = document.createElement("li");
li.textContent = item.title + " ";
const del = document.createElement("button");
del.type = "button";
del.textContent = "删除";
del.dataset.id = String(item.id);
li.appendChild(del);
list.appendChild(li);
}
}
document.getElementById("add").addEventListener("click", function () {
const input = document.getElementById("title");
const title = input.value.trim();
if (!title) {
alert("请输入内容"); // 入门演示;正式产品改用页面提示
return;
}
items.push({ id: Date.now(), title });
input.value = "";
render();
});
document.getElementById("list").addEventListener("click", function (e) {
const btn = e.target.closest("button[data-id]");
if (!btn) return;
const id = Number(btn.dataset.id);
const idx = items.findIndex((it) => it.id === id);
if (idx >= 0) {
items.splice(idx, 1);
render();
}
});
</script>
</body>
</html>
添加与删除都应只改数组再 render。这是综合练习课的缩略版预热。
讲清楚要点
- 因为脚本可能早于元素出现,所以把脚本放
body 底部,或绑定前检查元素是否存在。
- 因为
innerHTML 会解析 HTML,所以展示用户输入优先 textContent,避免 XSS。
- 因为表单默认会提交刷新,所以前端处理时要
preventDefault。
- 因为事件会冒泡,所以可在父元素委托监听,动态子节点也省得重绑。
- 因为可访问性与语义重要,所以能用
button/a 就别用假 div 点击。
- 因为界面应由数据驱动,所以先改数组/对象,再统一渲染 DOM。
你可能会懵的地方
-
querySelector 只返回第一个
多个用 querySelectorAll,再 for...of 循环。它返回的是「静态/类数组」节点列表,注意别当普通数组乱用(可 Array.from)。
-
绑定了为什么没反应?
查:id 是否写错、脚本是否报错、是否绑错元素、按钮是否在 form 里误提交导致刷新。
-
事件冒泡与捕获
默认多用冒泡阶段。委托就利用冒泡。捕获阶段入门可先不管。
-
replaceChildren 不支持?
旧环境可 list.innerHTML = ""(仅在无不可信 HTML 时)或循环 removeChild。
错法对照
| 错法 | 问题 | 正解 |
|---|
innerHTML 塞用户输入 | XSS 风险 | textContent / 安全处理 |
| 在 form 里用 button 不写 type | 误当提交 | type="button" 或处理 submit |
| 匿名函数无法移除监听 | 难清理 | 需要时保存函数引用再 removeEventListener |
| 只改 DOM 不改数据 | 状态不一致 | 数据优先,再渲染 |
| 给每个动态按钮单独绑且重复绑 | 泄漏/重复触发 | 事件委托 |
自测三问
- DOM 用一句话怎么解释?
- 为什么表单 submit 常要
preventDefault?
- 事件委托主要省了什么?对新加的子按钮有何好处?
小结一句话
通过 DOM 找到并改节点,用事件监听用户操作;改文本优先 textContent,提交记得 preventDefault,列表交互优先「数据 → 渲染」并善用委托。
延伸