Appearance
章节3 — DOM 操作与事件机制
一、章节概述
DOM(Document Object Model)是 JavaScript 操作网页的接口。本章系统讲解如何查询和操作 DOM 节点、绑定与处理事件、理解事件流与事件委托、掌握 BOM 浏览器对象模型,并通过 Ajax 与 Fetch 实现网络请求,最后探讨跨域问题的常见解决方案。
二、学习目标
| 目标分类 | 具体目标 |
|---|---|
| 知识目标 | 理解 DOM 树结构与事件流(捕获→目标→冒泡);掌握 BOM 核心对象;理解同源策略与跨域原理 |
| 技能目标 | 能使用 querySelector 与 classList 操作节点;能手写事件委托优化性能;能用 Fetch 发送并处理请求 |
| 素质目标 | 培养性能敏感的前端开发习惯,理解 Web 安全基础 |
三、核心知识点
3.1 DOM 节点查询与属性操作
3.1.1 DOM 树结构
document
└── html
├── head
│ ├── meta
│ └── title
└── body
├── header
├── main
│ ├── section
│ └── article
└── footer3.1.2 节点查询方法
| 方法 | 返回值 | 说明 |
|---|---|---|
document.querySelector(selector) | 第一个匹配元素 | 最灵活、推荐 |
document.querySelectorAll(selector) | NodeList(静态) | 匹配所有 |
document.getElementById(id) | 元素 | 按 ID 查询 |
document.getElementsByClassName(cls) | HTMLCollection(动态) | 按类名 |
element.closest(selector) | 最近的祖先 | 从自身向上查找 |
element.matches(selector) | boolean | 判断是否匹配 |
javascript
// 推荐用法
const nav = document.querySelector("nav");
const firstCard = document.querySelector(".card:first-child");
const allLinks = document.querySelectorAll("nav a");
// closest 实用场景:点击卡片获取整条数据
document.querySelectorAll(".card").forEach(card => {
card.addEventListener("click", (e) => {
const row = e.target.closest(".card"); // 无论点击内部何处
console.log(row.dataset.id);
});
});3.1.3 classList 操作
| 方法 | 说明 |
|---|---|
element.classList.add("active") | 添加类 |
element.classList.remove("active") | 删除类 |
element.classList.toggle("active") | 切换类 |
element.classList.contains("active") | 是否包含 |
javascript
const btn = document.querySelector("#menu-btn");
const menu = document.querySelector("#menu");
btn.addEventListener("click", () => {
menu.classList.toggle("open"); // 切换菜单展开
btn.classList.toggle("is-active"); // 按钮激活样式
});3.1.4 innerHTML 与文本操作
| 属性 | 说明 | 安全风险 |
|---|---|---|
element.innerHTML | 解析 HTML 字符串 | ⚠️ XSS 风险 |
element.textContent | 纯文本(安全) | ✅ 安全 |
element.innerText | 考虑 CSS 显示的文本 | 性能略差 |
element.setAttribute(name, val) | 设置属性 | — |
element.dataset.key | 读取 data-key 属性 | — |
javascript
// innerHTML 的使用场景(模板渲染)
const container = document.querySelector("#list");
const items = ["苹果", "香蕉", "橘子"];
// ✅ 安全的场景:硬编码或服务端过滤后的内容
container.innerHTML = items.map(item => `<li>${item}</li>`).join("");
// ❌ 危险场景:直接插入用户输入
// container.innerHTML = `<div>${userInput}</div>`; // XSS 漏洞!
// ✅ 安全替代:用 textContent
const comment = document.querySelector("#comment");
comment.textContent = userInput; // 不会解析 HTML3.2 事件绑定、冒泡与事件委托
3.2.1 事件绑定方式
javascript
// 方式1:on 属性(不推荐,只能绑定一个)
// btn.onclick = handleClick;
// 方式2:addEventListener ✅(推荐,可绑定多个)
const btn = document.querySelector("#btn");
btn.addEventListener("click", handleClick);
// 移除
// btn.removeEventListener("click", handleClick);3.2.2 事件流与事件对象
事件传播顺序:
捕获阶段:window → document → html → body → ... → 目标元素
目标阶段:抵达目标元素
冒泡阶段:目标元素 → ... → body → html → document → windowjavascript
document.querySelector("#parent").addEventListener(
"click",
(e) => {
console.log("捕获阶段执行", e.target, e.currentTarget);
},
true // 第三个参数 true = 捕获阶段触发
);
document.querySelector("#child").addEventListener("click", (e) => {
console.log("目标阶段", e.target);
// 阻止冒泡(谨慎使用)
// e.stopPropagation();
// 阻止默认行为
e.preventDefault(); // 常用于阻止表单提交 / 链接跳转
});3.2.3 事件委托(重要优化模式)
定义:利用事件冒泡,在父元素上统一监听子元素的事件,避免为每个子元素单独绑定。
javascript
// ❌ 不好的方式:为每个 li 单独绑定
document.querySelectorAll(".list-item").forEach(item => {
item.addEventListener("click", () => handleItem(item));
});
// ✅ 事件委托方式
document.querySelector("#list").addEventListener("click", (e) => {
const item = e.target.closest(".list-item");
if (!item) return; // 点击的不是列表项,忽略
console.log("点击了:", item.textContent, "数据ID:", item.dataset.id);
});
// 实用场景:动态列表
const list = document.querySelector("#todo-list");
list.addEventListener("click", (e) => {
if (e.target.classList.contains("delete-btn")) {
e.target.closest("li").remove();
}
if (e.target.classList.contains("edit-btn")) {
// 编辑逻辑
}
});事件委托优势:① 减少内存占用(尤其列表很长时);② 对新动态添加的元素自动生效。
3.3 BOM 浏览器对象与定时器
3.3.1 BOM 核心对象
| 对象 | 说明 |
|---|---|
window | 全局对象,代表浏览器窗口 |
document | DOM 的入口 |
navigator | 浏览器信息(User-Agent、地理位置) |
location | URL 相关信息 |
history | 浏览历史 |
screen | 屏幕信息 |
javascript
// location
console.log(location.href); // 当前完整 URL
console.log(location.pathname); // /course/dom
// 页面跳转
// location.href = "https://example.com";
// location.replace("https://example.com"); // 替换当前历史记录
// navigator
console.log(navigator.userAgent);
// 检测移动端
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
// history
history.back(); // 后退
history.forward(); // 前进
// history.pushState(state, title, url); // SPA 路由基础3.3.2 定时器
| API | 说明 | 清除方式 |
|---|---|---|
setTimeout(fn, ms) | 延迟执行一次 | clearTimeout(id) |
setInterval(fn, ms) | 间隔重复执行 | clearInterval(id) |
requestAnimationFrame(fn) | 下次重绘前执行(动画专用) | cancelAnimationFrame(id) |
javascript
// setTimeout
const timeoutId = setTimeout(() => {
console.log("3 秒后执行");
}, 3000);
// 清除
clearTimeout(timeoutId);
// setInterval + 倒计时示例
let count = 10;
const intervalId = setInterval(() => {
console.log(`倒计时:${count}秒`);
count--;
if (count < 0) {
clearInterval(intervalId);
console.log("倒计时结束!");
}
}, 1000);
// requestAnimationFrame — 平滑动画
function animate() {
element.style.transform = `translateX(${pos}px)`;
pos += 2;
if (pos < 500) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);3.4 Ajax 与 Fetch 网络请求
3.4.1 XMLHttpRequest(传统 Ajax)
javascript
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/users");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
// readyState: 0=UNSENT, 1=OPENED, 2=HEADERS_RECEIVED, 3=LOADING, 4=DONE
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log("成功:", data);
} else {
console.error("请求失败:", xhr.status);
}
}
};
xhr.onerror = () => console.error("网络错误");
xhr.send();3.4.2 Fetch API(现代方案)
Fetch 基于 Promise,语法更简洁,是当前主流方案。
javascript
// GET 请求
fetch("https://api.example.com/users")
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // 或 .text(), .blob()
})
.then((data) => console.log("用户数据:", data))
.catch((err) => console.error("请求失败:", err));
// POST 请求
fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "张三",
email: "zhangsan@example.com",
}),
})
.then((res) => res.json())
.then((data) => console.log("创建成功:", data));3.4.3 async/await 封装
javascript
// 通用请求封装
async function request(url, options = {}) {
const config = {
headers: { "Content-Type": "application/json" },
...options,
};
const response = await fetch(url, config);
if (!response.ok) {
const error = new Error(`请求失败:${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
}
// 使用
(async () => {
try {
const users = await request("https://api.example.com/users");
console.log(users);
} catch (err) {
console.error(err.message);
}
})();3.5 跨域问题与解决方案
3.5.1 同源策略
定义:浏览器安全机制,限制不同源(协议 + 域名 + 端口三个都相同)的页面之间进行数据交互。
https://example.com/page1
└── https://example.com/page2 ✅ 同源
└── http://example.com/page3 ❌ 协议不同
└── https://api.example.com ❌ 域名不同
└── https://example.com:8080 ❌ 端口不同3.5.2 解决方案对比
| 方案 | 原理 | 适用场景 |
|---|---|---|
| CORS | 服务端返回 Access-Control-Allow-Origin 头 | 标准、推荐 |
| JSONP | 利用 <script> 标签不受同源策略限制(仅 GET) | 兼容旧浏览器 |
| 代理服务器 | 同源代理转发请求 | 开发环境(Vite / webpack proxy) |
| postMessage | 不同窗口之间通信 | iframe 通信 |
3.5.3 CORS(官方推荐方案)
服务端需要设置响应头:
http
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true简单请求与预检请求:
javascript
// 简单请求(GET / POST + 简单头):直接发送
// 预检请求(PUT / DELETE / 自定义头):先发 OPTIONS
fetch("https://api.other.com/data", {
method: "PUT",
credentials: "include", // 携带 Cookie
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "value", // 触发预检
},
body: JSON.stringify({ key: "value" }),
});3.5.4 JSONP(仅 GET,兼容方案)
html
<!-- 客户端 -->
<script>
function handleData(data) {
console.log("JSONP 数据:", data);
}
</script>
<script src="https://api.other.com/data?callback=handleData"></script>
<!-- 服务端返回 -->
<!-- handleData({ "name": "张三", "age": 25 }) -->3.5.5 开发环境代理(Vite 配置)
javascript
// vite.config.js
export default {
server: {
proxy: {
"/api": {
target: "https://api.example.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
};四、小结
| 知识点 | 核心要点 |
|---|---|
| DOM 查询 | querySelector / querySelectorAll 是现代首选 |
| 属性操作 | classList.add/remove/toggle + textContent 安全操作 |
| 事件绑定 | addEventListener 支持捕获/冒泡;e.stopPropagation() 谨慎使用 |
| 事件委托 | 父元素统一监听,利用 e.target.closest() 精确定位 |
| BOM | location / navigator / history 控制浏览器行为 |
| 定时器 | setTimeout 延迟执行,setInterval 循环,requestAnimationFrame 动画 |
| Ajax / Fetch | Fetch 基于 Promise,推荐 async/await 封装 |
| 跨域 | CORS 是标准方案;JSONP 仅 GET;开发代理简化调试 |
五、练习
基础题
- 使用
querySelector选取页面中所有class="item"的元素,并为它们添加click事件,点击时通过classList.toggle("highlight")切换高亮。 - 用
setInterval实现一个数字时钟在页面上实时显示(格式:HH:MM:SS),带「暂停 / 继续」按钮。 - 使用 Fetch 从
https://jsonplaceholder.typicode.com/posts获取数据,用innerHTML渲染成文章列表。
进阶题
- 使用事件委托实现一个动态 Todo 列表:输入框添加新项,每项带有「删除」按钮(新添加的项删除按钮也能工作)。
- 用 async/await + Fetch 封装一个
http工具对象,包含get(url)和post(url, data)方法,并统一处理错误。
挑战题
- 实现一个图片懒加载组件:页面中所有
<img data-src="..." />在进入视口时再加载真实图片(使用IntersectionObserverAPI)。