Skip to content

章节3 — DOM 操作与事件机制


一、章节概述

DOM(Document Object Model)是 JavaScript 操作网页的接口。本章系统讲解如何查询和操作 DOM 节点、绑定与处理事件、理解事件流与事件委托、掌握 BOM 浏览器对象模型,并通过 Ajax 与 Fetch 实现网络请求,最后探讨跨域问题的常见解决方案。


二、学习目标

目标分类具体目标
知识目标理解 DOM 树结构与事件流(捕获→目标→冒泡);掌握 BOM 核心对象;理解同源策略与跨域原理
技能目标能使用 querySelectorclassList 操作节点;能手写事件委托优化性能;能用 Fetch 发送并处理请求
素质目标培养性能敏感的前端开发习惯,理解 Web 安全基础

三、核心知识点

3.1 DOM 节点查询与属性操作

3.1.1 DOM 树结构

document
 └── html
      ├── head
      │    ├── meta
      │    └── title
      └── body
           ├── header
           ├── main
           │    ├── section
           │    └── article
           └── footer

3.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; // 不会解析 HTML

3.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 → window
javascript
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全局对象,代表浏览器窗口
documentDOM 的入口
navigator浏览器信息(User-Agent、地理位置)
locationURL 相关信息
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() 精确定位
BOMlocation / navigator / history 控制浏览器行为
定时器setTimeout 延迟执行,setInterval 循环,requestAnimationFrame 动画
Ajax / FetchFetch 基于 Promise,推荐 async/await 封装
跨域CORS 是标准方案;JSONP 仅 GET;开发代理简化调试

五、练习

基础题

  1. 使用 querySelector 选取页面中所有 class="item" 的元素,并为它们添加 click 事件,点击时通过 classList.toggle("highlight") 切换高亮。
  2. setInterval 实现一个数字时钟在页面上实时显示(格式:HH:MM:SS),带「暂停 / 继续」按钮。
  3. 使用 Fetch 从 https://jsonplaceholder.typicode.com/posts 获取数据,用 innerHTML 渲染成文章列表。

进阶题

  1. 使用事件委托实现一个动态 Todo 列表:输入框添加新项,每项带有「删除」按钮(新添加的项删除按钮也能工作)。
  2. 用 async/await + Fetch 封装一个 http 工具对象,包含 get(url)post(url, data) 方法,并统一处理错误。

挑战题

  1. 实现一个图片懒加载组件:页面中所有 <img data-src="..." /> 在进入视口时再加载真实图片(使用 IntersectionObserver API)。

Python 学习资料