前四章的材料(HTML 结构、CSS 样式、JS 语言)在这章合体:DOM 是浏览器把 HTML 解析成的可编程对象树,JS 通过它读写页面;事件是页面对用户动作的反应机制。学完这章,你就能写「会动」的页面了。本章的重头戏是事件流与事件委托,委托模式不只是省代码,它是理解 React 事件系统(第 8 章)的铺垫。
浏览器解析 HTML 后生成 DOM(Document Object Model)树,F12 的 Elements 面板就是它的可视化:
document
└─ html
└─ body
└─ ul#list
├─ li.item "A"
└─ li.item "B"
节点间是父子、兄弟关系;标签是元素节点,文字是文本节点。一切页面操作的本质,都是对这棵树的查询和改写。
// 查:querySelector 全家桶(现代首选)
const list = document.querySelector("#list");
const items = document.querySelectorAll(".item"); // 全部匹配
// 建 + 挂
const li = document.createElement("li");
li.textContent = "C";
list.append(li);
// 改
li.classList.add("active"); // class 操作(比 className 赋值精细)
li.dataset.id = "3"; // data-* 属性,dataset 自动映射
li.style.color = "red"; // 内联样式(个别动态样式才用)
// 删
li.remove();
选择风格统一用 querySelector(All),它们吃 CSS 选择器语法(第 2 章白学了,直接复用)。一个易混点:querySelectorAll 返回静态快照(查完之后新增的元素不在里面),getElementsByClassName 返回动态集合(实时跟着 DOM 变);写现代代码统一 querySelectorAll,按需重查即可。
改内容的老大难:textContent(纯文本,安全)vs innerHTML(解析 HTML,方便但危险)。危险在哪:innerHTML 拼接用户输入等于让用户在你的页面里注入标签,<img src=x onerror="偷数据()"> 这类 XSS 攻击(跨站脚本,计网课第 8 章安全部分)就是这么进的门。纪律:用户输入一律 textContent 或先转义,innerHTML 只用于可信内容。
点击一个 li,事件不是「只发生在这里」,它走完整条路线:
捕获阶段(外→内):document → body → ul → li ↓
目标阶段: li(事件真正的主人)
冒泡阶段(内→外):li → ul → body → document ↑
addEventListener 默认在冒泡阶段监听(第三参 false),传 true 则捕获阶段监听:
list.addEventListener("click", e => console.log("冒泡:我是 ul"));
document.body.addEventListener("click", () => console.log("冒泡:我是 body"));
// 点击 li:先 "冒泡:我是 ul" 再 "冒泡:我是 body"(从内向外)
为什么默认冒泡?因为委托(下一节)靠它成立。冒泡让「点击 li」同时成为「点击了 ul 里」的事件,外层才有机会代为处理。
需求:给列表的每一项加点击行为。笨办法给每个 li 各绑一个监听器(100 项 100 个,动态新增还要补绑);委托的办法是只给父元素绑一个,靠冒泡统一接单:
list.addEventListener("click", (e) => {
const item = e.target.closest("li"); // e.target 是真正被点的元素
if (item) console.log(item.textContent);
});
两个对象必须分清:e.target 是「被点的那个元素」,e.currentTarget 是「绑了监听器的元素」(委托里通常是父级)。委托的收益:监听器从 O(n) 降到 O(1)、动态新增的子项天然被覆盖(监听器在父级,新增子项不用管)、销毁方便(只解绑一个)。React 的合成事件系统整个建立在委托思想上(它把事件统一委托到根节点,第 8 章),所以委托不是奇技淫巧,是主流框架的地基。
link.addEventListener("click", (e) => {
e.preventDefault(); // 拦下浏览器默认动作(不跳转/不提交)
e.stopPropagation(); // 拦下继续冒泡(外层监听器收不到)
});
两者独立:preventDefault 管「浏览器默认行为」(链接跳转、表单提交、右键菜单),stopPropagation 管「事件传播」。表单的典型组合:submit 监听里 preventDefault 后自己用 JS 校验提交(无刷新提交)。委托场景里慎用 stopPropagation:它可能让更外层的委托收不到事件,拆他人大脑。
<img src=x onerror="alert(1)"> 看弹窗,再换 textContent 验证被当纯文本;理解「注入」的形状。