一、DOM 基础
DOM(Document Object Model,文档对象模型)是 JavaScript 操作 HTML 和 XML 文档的编程接口。它将网页文档解析为树形结构,使开发者能够动态地访问和修改页面内容、结构和样式。
1.1 DOM 树结构
DOM 树以 document 为根节点,包含以下几类节点:
| 节点类型 | NodeType 值 | 描述 |
|---|---|---|
Document |
9 | 整个文档的根节点 |
Element |
1 | HTML 元素节点 |
Attr |
2 | 元素的属性节点(已弃用,建议用 getAttribute) |
Text |
3 | 元素中的文本内容 |
Comment |
8 | HTML 注释节点 |
DocumentFragment |
11 | 轻量文档片段 |
// 检查节点类型
const div = document.createElement('div');
console.log(div.nodeType); // 1 (Element)
console.log(div.nodeName); // "DIV"
const text = document.createTextNode('Hello');
console.log(text.nodeType); // 3 (Text)
const comment = document.createComment('注释');
console.log(comment.nodeType); // 8 (Comment)
console.log(document.nodeType); // 9 (Document)
console.log(DocumentFragment.prototype.nodeType); // 111.2 Node 接口 vs Element 接口
Node 是所有 DOM 节点类型的基接口,而 Element 继承自 Node,专注于 HTML 元素的操作。
// Node 接口提供的属性和方法(所有节点都可用)
const el = document.querySelector('div');
// Node 层面的属性
el.nodeType; // 1
el.nodeName; // "DIV"
el.parentNode; // 父节点(可能是 Element 或 DocumentFragment)
el.childNodes; // NodeList(包含所有子节点,含文本和注释)
el.firstChild; // 第一个子节点(可能是文本节点)
el.lastChild; // 最后一个子节点
el.previousSibling;
el.nextSibling;
el.textContent; // 所有后代文本内容
// Element 层面的属性(仅 Element 节点可用)
el.tagName; // "DIV"(类似 nodeName)
el.id;
el.className;
el.children; // HTMLCollection(仅包含元素子节点)
el.firstElementChild;
el.lastElementChild;
el.previousElementSibling;
el.nextElementSibling;
el.innerHTML;
el.outerHTML;关键区别:childNodes 包含文本节点和注释节点,而 children 只包含元素节点。
// childNodes vs children 的差异
const ul = document.querySelector('ul');
// HTML: <ul><li>A</li><li>B</li></ul>
console.log(ul.childNodes.length); // 5: [text, li, text, li, text]
// 两个 <li> 之间的空白字符会被解析为 text 节点
console.log(ul.children.length); // 2: [li, li]1.3 节点导航
节点导航分为 Node 版本 和 Element 版本,两者行为不同:
// Node 版导航(包含文本和注释)
const container = document.getElementById('container');
container.parentNode; // 父节点
container.childNodes; // 所有子节点(NodeList)
container.firstChild; // 第一个子节点
container.lastChild; // 最后一个子节点
container.previousSibling; // 前一个兄弟节点
container.nextSibling; // 后一个兄弟节点
// Element 版导航(仅包含 Element 节点)
container.parentElement; // 父元素(必须是 Element,否则返回 null)
container.children; // 所有子元素(HTMLCollection,实时集合)
container.firstElementChild; // 第一个子元素
container.lastElementChild; // 最后一个子元素
container.previousElementSibling; // 前一个兄弟元素
container.nextElementSibling; // 后一个兄弟元素// 实战:遍历纯元素节点(跳过文本与注释)
function walkElementChildren(element, callback) {
let child = element.firstElementChild;
while (child) {
callback(child);
child = child.nextElementSibling;
}
}1.4 查询方法
经典查询方法
// 1. getElementById — 最快,返回单个元素或 null
const header = document.getElementById('header');
// 2. getElementsByClassName — 返回 HTMLCollection(实时集合)
const items = document.getElementsByClassName('item');
console.log(items.length); // 当 DOM 变化时自动更新
// 3. getElementsByTagName — 返回 HTMLCollection(实时集合)
const paragraphs = document.getElementsByTagName('p');现代查询方法(CSS 选择器)
// 4. querySelector — 返回匹配的第一个元素或 null
const firstBtn = document.querySelector('.btn.primary');
const navItem = document.querySelector('#nav > li:first-child');
// 5. querySelectorAll — 返回静态 NodeList
const allBtns = document.querySelectorAll('.btn');
console.log(allBtns.length); // 静态快照,DOM 变化不影响它
console.log(allBtns[0]); // 支持 forEach,但不支持 map
allBtns.forEach(btn => btn.classList.add('active'));
// 6. closest — 从当前元素向上查找最近的匹配祖先(含自身)
const card = event.target.closest('.card');
if (card) {
console.log('点击发生在卡片内:', card.id);
}
// 7. matches — 检查元素是否匹配某个选择器
function isButton(el) {
return el.matches('button, [role="button"], input[type="submit"]');
}1.5 实时集合 vs 静态集合
| 特性 | HTMLCollection(实时) | NodeList(querySelectorAll) |
|---|---|---|
| 更新方式 | DOM 变化自动反映 | 静态快照 |
| 索引访问 | ✅ | ✅ |
| forEach | ❌ | ✅ |
| 长度变化 | 随 DOM 自动变化 | 固定 |
// 实时集合的陷阱
const liveItems = document.getElementsByClassName('item');
console.log('初始数量:', liveItems.length); // 3
// 移除一个元素
liveItems[0].remove();
console.log('移除后数量:', liveItems.length); // 2(自动更新)
// 静态集合不受影响
const staticItems = document.querySelectorAll('.item');
console.log('静态集合:', staticItems.length); // 始终是查询时的快照二、节点操作
2.1 创建节点
// 1. 创建元素节点
const div = document.createElement('div');
div.id = 'new-div';
div.className = 'box';
// 2. 创建文本节点
const text = document.createTextNode('Hello World');
// 3. 创建注释节点
const comment = document.createComment('这是注释');
// 4. 创建文档片段(批量操作优化)
const fragment = document.createDocumentFragment();2.2 插入节点
传统方法
const parent = document.getElementById('list');
const newItem = document.createElement('li');
newItem.textContent = '新增项';
// appendChild — 在末尾追加
parent.appendChild(newItem);
// insertBefore — 在指定子节点前插入
const referenceItem = parent.children[0];
parent.insertBefore(newItem, referenceItem);现代方法(更直观)
const parent = document.getElementById('list');
const item = document.createElement('li');
item.textContent = '新项';
// append — 在末尾追加(支持多个参数和字符串)
parent.append(item);
parent.append('直接文本', document.createElement('span'));
// prepend — 在开头插入
parent.prepend(item);
// after — 在当前元素之后插入兄弟元素
const current = document.getElementById('current');
current.after(document.createElement('hr'));
// before — 在当前元素之前插入兄弟元素
current.before(document.createElement('hr'));传统 vs 现代对比:
| 方法 | 返回值 | 多节点支持 | 字符串处理 |
|---|---|---|---|
appendChild |
插入的节点 | ❌ | ❌ |
insertBefore |
插入的节点 | ❌ | ❌ |
append |
undefined |
✅ | 自动转文本节点 |
prepend |
undefined |
✅ | 自动转文本节点 |
after |
undefined |
✅ | 自动转文本节点 |
before |
undefined |
✅ | 自动转文本节点 |
2.3 替换节点
const old = document.getElementById('old');
const newEl = document.createElement('div');
newEl.textContent = '新元素';
// replaceChild — 父元素调用,替换某个子节点
old.parentNode.replaceChild(newEl, old);
// replaceWith — 元素自身调用(更简洁)
old.replaceWith(newEl);
// replaceChildren — 替换所有子节点
const container = document.getElementById('container');
container.replaceChildren(
document.createElement('h2'),
document.createElement('p')
);2.4 删除节点
const element = document.getElementById('to-remove');
// removeChild — 父元素调用删除某个子节点,返回被删除的节点
const removed = element.parentNode.removeChild(element);
// remove — 元素自身删除(更简洁,无返回值)
element.remove();
// 清空所有子节点
const parent = document.getElementById('parent');
// 方法1:逐个删除
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
// 方法2:直接清空 innerHTML(有内存泄漏风险)
parent.innerHTML = '';
// 方法3:replaceChildren(推荐)
parent.replaceChildren();2.5 cloneNode
const original = document.getElementById('original');
// 浅克隆 — 仅复制元素本身,不包含子节点
const shallowClone = original.cloneNode(false);
// 深克隆 — 复制元素及其所有后代
const deepClone = original.cloneNode(true);
// 注意事项:
// 1. 克隆不会复制事件监听器
// 2. 克隆不会复制 data 属性(绑定在 JS 对象上的数据)
// 3. id 属性会被复制,可能导致重复 id
const clone = original.cloneNode(true);
clone.id = original.id + '-clone'; // 记得修改 id2.6 DocumentFragment 批量操作优化
DocumentFragment 是一个轻量级的文档对象,存在于内存中而非真实 DOM 树。批量插入时使用它可避免多次触发浏览器重排。
// ❌ 低效方式:每次 appendChild 都触发重排
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `项 ${i}`;
list.appendChild(li); // 每次操作都触发 reflow
}
// ✅ 高效方式:使用 DocumentFragment 批量插入
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `项 ${i}`;
fragment.appendChild(li); // 操作在内存中完成
}
list.appendChild(fragment); // 仅触发一次 reflow
// ✅ 现代方式:append 也支持批量(但仍推荐 fragment 避免大数组)
list.append(...items); // 参数展开可能爆栈,适合中量操作三、元素属性与内容
3.1 innerHTML vs outerHTML vs textContent vs innerText
const el = document.getElementById('demo');
// 假设 HTML: <div id="demo">Hello <b>World</b></div>
// innerHTML — 获取/设置元素的 HTML 内容(会解析 HTML 字符串)
console.log(el.innerHTML); // "Hello <b>World</b>"
el.innerHTML = '<span>新内容</span>'; // 解析并替换 HTML
// outerHTML — 包含元素自身的 HTML
console.log(el.outerHTML);
// "<div id="demo">Hello <b>World</b></div>"
el.outerHTML = '<section>替换</section>'; // 用新元素替换自身
// textContent — 所有文本内容(忽略标签,包含隐藏元素的文本)
console.log(el.textContent); // "Hello World"
el.textContent = '纯文本'; // 设置纯文本,不会解析 HTML 标签
// innerText — 可见文本(考虑了 CSS 样式,性能较慢)
console.log(el.innerText); // "Hello World"(不包含 display:none 的文本)
el.innerText = '可见文本'; // 设置可见文本关键差异总结:
| 属性 | 解析 HTML | 性能 | 包含隐藏文本 | 返回值 |
|---|---|---|---|---|
innerHTML |
✅ | 较慢(需解析) | ✅ | HTML 字符串 |
outerHTML |
✅ | 较慢 | ✅ | 包含自身的 HTML |
textContent |
❌ | 快 | ✅ | 纯文本 |
innerText |
❌ | 慢(触发布局) | ❌ | 可见纯文本 |
// 实战:安全地设置纯文本(防止 XSS)
function setTextSafely(element, text) {
element.textContent = text; // 自动转义 HTML 特殊字符
// ❌ element.innerHTML = text; // 不安全!
}
// 实战:获取所有可见文本
function getVisibleText(element) {
// innerText 会自动忽略 display:none 的子元素文本
return element.innerText.trim();
}3.2 getAttribute / setAttribute / removeAttribute
const link = document.querySelector('a');
// 获取属性
console.log(link.getAttribute('href'));
console.log(link.getAttribute('target'));
// 设置属性
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
// 删除属性
link.removeAttribute('target');
// 检查属性是否存在
console.log(link.hasAttribute('href')); // true
// 获取所有属性
const attrs = link.attributes; // NamedNodeMap
for (const attr of attrs) {
console.log(`${attr.name}: ${attr.value}`);
}属性(Attribute)vs 属性(Property)的区别:
const input = document.querySelector('input[type="checkbox"]');
// Attribute 是 HTML 上的初始值
input.getAttribute('checked'); // ""(存在即返回空字符串)
// Property 是 DOM 对象的当前状态
input.checked; // false(未选中时)
// 两者不同步的场景:
input.setAttribute('value', '初始值');
console.log(input.value); // ""(如果用户已输入内容,property 不同步)
input.value = '用户输入';
console.log(input.getAttribute('value')); // "初始值"(attribute 不变)3.3 dataset(data-* 属性)
HTML5 提供的 data-* 自定义属性,通过 dataset API 便捷访问:
// HTML: <div id="card" data-id="123" data-type="article" data-user-name="张三"></div>
const card = document.getElementById('card');
// 读取 data-* 属性
console.log(card.dataset.id); // "123"
console.log(card.dataset.type); // "article"
console.log(card.dataset.userName); // "张三"(驼峰转换)
// 设置 data-* 属性
card.dataset.status = 'active';
// HTML 变为: <div ... data-status="active"></div>
card.dataset.userAge = '28';
// HTML 变为: <div ... data-user-age="28"></div>
// 删除 data-* 属性
delete card.dataset.type;
// 判断是否存在
console.log('id' in card.dataset); // true
// 遍历所有 data-* 属性
for (const key in card.dataset) {
console.log(`data-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${card.dataset[key]}`);
}命名转换规则:
| HTML(data-*) | dataset 属性 |
|---|---|
data-user-name |
dataset.userName |
data-id |
dataset.id |
data-json-data |
dataset.jsonData |
3.4 classList
classList 提供了比 className 更强大、更安全的类操作:
const el = document.getElementById('element');
// classList API
el.classList.add('active', 'highlight'); // 添加一个或多个类
el.classList.remove('inactive'); // 移除一个类
el.classList.toggle('visible'); // 切换类(存在则删除,不存在则添加)
el.classList.contains('active'); // 检查是否包含某个类 → boolean
el.classList.replace('old', 'new'); // 替换类名
// 批量操作
el.classList.add(...['class1', 'class2']);
// 遍历 classList
el.classList.forEach(cls => console.log(cls));
// 获取类名数量
console.log(el.classList.length);
// className 方式(字符串操作,较原始)
el.className = 'active highlight'; // 覆盖所有类名
el.className += ' new-class'; // 追加(注意空格)
const hasClass = el.className.includes('active'); // 不精确,可能误匹配实战:常见 class 操作模式:
// 1. 条件性添加类
const isDisabled = true;
el.classList.toggle('disabled', isDisabled); // 第二个参数强制添加/移除
// 2. 删除多个指定类
el.classList.remove('btn', 'btn-primary', 'btn-lg');
// 3. 复合状态管理
function updateState(el, state) {
el.classList.remove('loading', 'error', 'success');
if (state) el.classList.add(state);
}
// 4. className 与 classList 的性能选择
// className 在需要完全重写时更快(避免多次 DOM 写入)
// classList 在需要部分修改时更安全四、样式操作
4.1 style 属性(行内样式)
通过 element.style 操作行内样式,对应 HTML 的 style 属性:
const box = document.getElementById('box');
// 设置单个样式属性(使用驼峰命名)
box.style.backgroundColor = '#3498db';
box.style.width = '200px';
box.style.height = '200px';
box.style.borderRadius = '8px';
box.style.transition = 'all 0.3s ease';
// CSS 属性名转换规则
// CSS 中带连字符的属性 → 驼峰命名
// background-color → backgroundColor
// font-size → fontSize
// border-top-left-radius → borderTopLeftRadius
// -webkit-transform → WebkitTransform(首字母大写)
// 设置 CSS 变量(CSS 自定义属性)
box.style.setProperty('--primary-color', '#e74c3c');
box.style.setProperty('--spacing', '16px');
// 获取行内样式值
console.log(box.style.backgroundColor);
console.log(box.style.width);
// 获取 CSS 变量的值(通过 getPropertyValue)
const value = box.style.getPropertyValue('--primary-color');
// 移除行内样式属性
box.style.removeProperty('background-color');
box.style.removeProperty('width');
// 批量设置样式
Object.assign(box.style, {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
margin: '10px 0'
});
// 或者使用 cssText(一次性设置全部样式)
box.style.cssText = 'display: flex; align-items: center; gap: 12px;';
// ⚠️ cssText 会覆盖所有已有的行内样式4.2 getComputedStyle(计算样式)
getComputedStyle 获取最终生效的样式值(包含所有 CSS 来源:行内、样式表、继承等):
const element = document.getElementById('my-element');
// 获取计算样式
const computed = window.getComputedStyle(element);
// 读取计算后的属性值
console.log(computed.display); // "flex"(实际生效的值)
console.log(computed.width); // "960px"(含单位的像素值)
console.log(computed.fontSize); // "16px"
console.log(computed.backgroundColor); // "rgb(52, 152, 219)"
// 使用 getComputedStyle 获取伪元素样式
const pseudoStyle = window.getComputedStyle(element, '::before');
console.log(pseudoStyle.content);
console.log(pseudoStyle.color);
// 获取 CSS 自定义属性的计算值
const root = document.documentElement;
const rootStyle = getComputedStyle(root);
const primaryColor = rootStyle.getPropertyValue('--primary-color').trim();
console.log(primaryColor); // "#3498db"
// getComputedStyle vs element.style
// element.style → 仅读取或设置行内样式(可写)
// getComputedStyle → 读取最终计算样式(只读)4.3 className vs classList
| 场景 | className | classList |
|---|---|---|
| 完全重写类名 | ✅ 简单直接 | ❌ 需要先清除再添加 |
| 追加单个类 | el.className += ' cls'(易出错) |
✅ el.classList.add('cls') |
| 删除单个类 | 需要字符串替换 | ✅ el.classList.remove('cls') |
| 切换类 | 需要手动判断 | ✅ el.classList.toggle('cls') |
| 检查是否包含 | el.className.includes('cls')(可能误匹配) |
✅ el.classList.contains('cls') |
| 批量类名 | ✅ 字符串一次写入 | 需要逐个添加 |
// className 适合完全重写的场景
function setTheme(theme) {
document.body.className = `theme-${theme}`;
}
// classList 适合部分修改的场景
function toggleNav() {
document.querySelector('.nav').classList.toggle('nav--open');
}4.4 CSS 自定义属性(CSS 变量)的 JS 操作
CSS 变量通过 -- 前缀定义,JS 中通过 setProperty 和 getPropertyValue 操作:
// 设置 CSS 变量
const root = document.documentElement;
// 设置全局变量
root.style.setProperty('--primary', '#3498db');
root.style.setProperty('--spacing-unit', '8px');
root.style.setProperty('--font-size-base', '16px');
// 获取 CSS 变量
const primary = getComputedStyle(root).getPropertyValue('--primary').trim();
console.log(primary); // "#3498db"
// 响应式主题切换
function setTheme(themeName) {
const root = document.documentElement;
if (themeName === 'dark') {
root.style.setProperty('--bg', '#1a1a2e');
root.style.setProperty('--text', '#eee');
root.style.setProperty('--primary', '#e94560');
} else {
root.style.setProperty('--bg', '#fff');
root.style.setProperty('--text', '#333');
root.style.setProperty('--primary', '#3498db');
}
}
// 在元素级别覆盖变量
const card = document.querySelector('.card');
card.style.setProperty('--primary', '#2ecc71');五、事件机制
5.1 事件流
DOM 事件流分为三个阶段:
┌──────────────────────┐
│ window │
│ (捕获阶段开始) │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ document │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ <html> │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ <body> │
└────────┬─────────────┘
│ 捕获阶段(capture phase)
┌────────▼─────────────┐
│ <div id="outer"> │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ <div id="inner"> │ ← 目标阶段(target phase)
└────────┬─────────────┘
│ 冒泡阶段(bubble phase)
┌────────▼─────────────┐
│ <div id="outer"> │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ <body> │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ document │
└────────┬─────────────┘
│
┌────────▼─────────────┐
│ window │
│ (冒泡阶段结束) │
└──────────────────────┘// 1. 捕获阶段监听(第三个参数传 true)
document.getElementById('outer').addEventListener('click', (e) => {
console.log('捕获阶段 - outer');
}, true); // capture: true
// 2. 冒泡阶段监听(第三个参数 false 或省略)
document.getElementById('inner').addEventListener('click', (e) => {
console.log('冒泡阶段 - inner');
}, false);
// 3. 仅捕获阶段执行,冒泡阶段忽略
document.addEventListener('click', handler, { capture: true });5.2 addEventListener 选项
addEventListener 的第三个参数可以是布尔值或选项对象:
const btn = document.getElementById('btn');
const handler = () => console.log('clicked');
// 选项对象详解
btn.addEventListener('click', handler, {
capture: false, // 是否在捕获阶段触发
once: true, // 仅触发一次后自动移除
passive: true, // 告诉浏览器不会调用 preventDefault(提升滚动性能)
signal: abortController.signal // 通过 AbortController 移除
});
// once 的妙用:一次性初始化
function init() {
console.log('初始化,仅执行一次');
document.addEventListener('DOMContentLoaded', init, { once: true });
}
// passive 的典型场景:滚动事件优化
// 不添加 passive 时,浏览器必须等待 JS 执行完才知道有没有 preventDefault
// 加了 passive: true,浏览器可立即滚动,无需等待
document.addEventListener('touchmove', (e) => {
// 处理触摸移动,但不调用 preventDefault
}, { passive: true });
// 使用 AbortController 移除事件
const controller = new AbortController();
const { signal } = controller;
document.addEventListener('click', handler1, { signal });
document.addEventListener('scroll', handler2, { signal });
// 一次性移除所有关联事件
controller.abort();事件监听器移除的三种方式:
// 方式1:removeEventListener(需要同一个函数引用)
function handleClick() { console.log('click'); }
btn.addEventListener('click', handleClick);
btn.removeEventListener('click', handleClick); // 必须传入同一个函数
// 方式2:once 选项(自动移除)
btn.addEventListener('click', () => console.log('仅一次'), { once: true });
// 方式3:AbortController(批量移除)
const ac = new AbortController();
btn.addEventListener('click', handler, { signal: ac.signal });
ac.abort(); // 移除 signal 绑定的所有监听器5.3 事件对象
element.addEventListener('click', (event) => {
// 事件对象的基本属性
console.log(event.type); // "click"
console.log(event.target); // 实际触发事件的元素
console.log(event.currentTarget); // 绑定监听器的元素(this 通常指向它)
console.log(event.eventPhase); // 1=捕获, 2=目标, 3=冒泡
console.log(event.bubbles); // 是否可冒泡
console.log(event.cancelable); // 是否可取消默认行为
console.log(event.defaultPrevented); // 是否调用了 preventDefault
console.log(event.timestamp); // 事件创建的时间戳
// target vs currentTarget 的区别
// 假设: <div id="outer"><button id="btn">点击</button></div>
// outer 上绑定了 click 事件,点击按钮时:
// event.target → <button id="btn">
// event.currentTarget → <div id="outer">(绑定事件的那个元素)
// 阻止默认行为(如链接跳转、表单提交)
event.preventDefault();
// 阻止事件冒泡(停止向上传播)
event.stopPropagation();
// 阻止事件传播 + 阻止同元素上的其他监听器
event.stopImmediatePropagation();
// 区别:stopPropagation 只阻止传播到父元素,
// stopImmediatePropagation 还阻止自身元素上后续绑定的监听器
});
// 示例:stopImmediatePropagation 的效果
el.addEventListener('click', () => console.log('第一个'));
el.addEventListener('click', (e) => {
e.stopImmediatePropagation(); // 阻止后续监听器
console.log('第二个');
});
el.addEventListener('click', () => console.log('第三个')); // 不会执行5.4 事件委托
事件委托利用事件冒泡机制,在父元素上统一处理子元素的事件,减少监听器数量:
// ❌ 低效方式:每个 li 绑定一个监听器
document.querySelectorAll('.list-item').forEach(item => {
item.addEventListener('click', () => {
console.log(item.dataset.id);
});
});
// ✅ 高效方式:事件委托,只绑一个监听器
document.getElementById('list').addEventListener('click', (e) => {
// 查找实际点击的列表项
const item = e.target.closest('.list-item');
if (!item) return; // 点击的不是列表项,忽略
console.log('点击了:', item.dataset.id);
// 根据 data-* 属性分发处理
const action = item.dataset.action;
switch (action) {
case 'edit':
editItem(item.dataset.id);
break;
case 'delete':
deleteItem(item.dataset.id);
break;
}
});
// 事件委托的优势:
// 1. 减少内存占用(一个监听器 vs N 个监听器)
// 2. 对新插入的元素自动生效(动态列表)
// 实战:可编辑表格的事件委托
document.querySelector('table').addEventListener('click', (e) => {
const cell = e.target.closest('td');
if (!cell) return;
if (cell.classList.contains('editable')) {
// 使单元格可编辑
const input = document.createElement('input');
input.value = cell.textContent.trim();
cell.textContent = '';
cell.appendChild(input);
input.focus();
input.addEventListener('blur', () => {
cell.textContent = input.value;
}, { once: true });
}
});5.5 自定义事件
使用 CustomEvent 创建和派发自定义事件:
// 1. 创建自定义事件(可附带数据)
const event = new CustomEvent('user-login', {
detail: {
userId: 123,
username: '张三',
timestamp: Date.now()
},
bubbles: true, // 是否冒泡
cancelable: true // 是否可取消
});
// 2. 监听自定义事件
document.addEventListener('user-login', (e) => {
console.log('用户登录:', e.detail);
// { userId: 123, username: '张三', timestamp: ... }
});
// 3. 派发事件
document.dispatchEvent(event);
// 实战:组件通信使用自定义事件
class Toast {
show(message, type = 'info') {
const event = new CustomEvent('toast', {
detail: { message, type, id: Date.now() },
bubbles: true
});
document.dispatchEvent(event);
}
}
// 在 UI 层监听
document.addEventListener('toast', (e) => {
const { message, type, id } = e.detail;
const toastEl = document.createElement('div');
toastEl.className = `toast toast--${type}`;
toastEl.textContent = message;
toastEl.dataset.id = id;
document.body.appendChild(toastEl);
setTimeout(() => toastEl.remove(), 3000);
});
// 使用
const toast = new Toast();
toast.show('操作成功!', 'success');
toast.show('网络错误', 'error');5.6 常见事件分类
鼠标事件
element.addEventListener('click', handler); // 点击
element.addEventListener('dblclick', handler); // 双击
element.addEventListener('mousedown', handler); // 鼠标按下
element.addEventListener('mouseup', handler); // 鼠标释放
element.addEventListener('mousemove', handler); // 鼠标移动
element.addEventListener('mouseenter', handler); // 鼠标进入(不冒泡)
element.addEventListener('mouseleave', handler); // 鼠标离开(不冒泡)
element.addEventListener('mouseover', handler); // 鼠标进入(冒泡)
element.addEventListener('mouseout', handler); // 鼠标离开(冒泡)
element.addEventListener('contextmenu', handler); // 右键菜单
// 事件对象属性
element.addEventListener('click', (e) => {
console.log(e.clientX, e.clientY); // 视口坐标
console.log(e.pageX, e.pageY); // 页面坐标
console.log(e.screenX, e.screenY); // 屏幕坐标
console.log(e.button); // 0=左键, 1=中键, 2=右键
console.log(e.ctrlKey, e.shiftKey, e.altKey, e.metaKey); // 修饰键
});键盘事件
document.addEventListener('keydown', (e) => {
console.log(e.key); // "a", "Enter", "ArrowUp"
console.log(e.code); // "KeyA", "Enter", "ArrowUp"
console.log(e.keyCode); // 已弃用,不推荐使用
console.log(e.repeat); // 是否长按重复触发
// 组合键判断
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('保存快捷键');
}
if (e.metaKey && e.key === 'k') {
e.preventDefault();
console.log('Mac 的 Cmd+K');
}
});
document.addEventListener('keyup', handler);
document.addEventListener('keypress', handler); // 已弃用,用 keydown 替代表单事件
const form = document.querySelector('form');
const input = document.querySelector('input');
form.addEventListener('submit', (e) => {
e.preventDefault(); // 阻止表单提交刷新页面
const formData = new FormData(form);
console.log([...formData.entries()]);
});
input.addEventListener('focus', () => console.log('获得焦点'));
input.addEventListener('blur', () => console.log('失去焦点'));
input.addEventListener('input', (e) => console.log(e.target.value)); // 值变化
input.addEventListener('change', (e) => console.log('值已确认')); // 失焦且值变化
input.addEventListener('invalid', (e) => console.log('验证失败'));
// 输入防抖(debounce)
let debounceTimer;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
console.log('发送搜索请求:', input.value);
}, 300);
});焦点事件
element.addEventListener('focus', handler); // 获得焦点(不冒泡)
element.addEventListener('blur', handler); // 失去焦点(不冒泡)
element.addEventListener('focusin', handler); // 获得焦点(冒泡)
element.addEventListener('focusout', handler); // 失去焦点(冒泡)
// 焦点管理示例
document.addEventListener('focusin', (e) => {
console.log('当前焦点元素:', e.target);
});
document.addEventListener('focusout', (e) => {
console.log('焦点移出:', e.target);
console.log('移入:', e.relatedTarget); // 新获得焦点的元素
});滚动与触摸事件
// 滚动事件
window.addEventListener('scroll', () => {
console.log('滚动位置:', window.scrollY);
}, { passive: true }); // 必须加 passive 提升性能
// 触摸事件(移动端)
element.addEventListener('touchstart', (e) => {
console.log('触摸点:', e.touches);
console.log('触摸目标:', e.targetTouches);
console.log('已改变的触摸:', e.changedTouches);
});
element.addEventListener('touchmove', (e) => {
// 阻止页面滚动
e.preventDefault();
}, { passive: false });
element.addEventListener('touchend', (e) => {
console.log('触摸结束');
// 判断滑动方向
const touch = e.changedTouches[0];
console.log('结束位置:', touch.clientX, touch.clientY);
});拖拽事件
// 拖拽源元素事件
dragElement.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', dragElement.id);
e.dataTransfer.effectAllowed = 'move';
});
dragElement.addEventListener('drag', () => console.log('拖拽中'));
dragElement.addEventListener('dragend', () => console.log('拖拽结束'));
// 放置目标元素事件
dropZone.addEventListener('dragenter', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // 必须阻止才能允许放置
e.dataTransfer.dropEffect = 'move';
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const id = e.dataTransfer.getData('text/plain');
const draggedEl = document.getElementById(id);
dropZone.appendChild(draggedEl);
});六、BOM(浏览器对象模型)
BOM(Browser Object Model)提供与浏览器本身交互的对象,核心是 window 对象。
6.1 window 对象
window 是 BOM 的顶层对象,代表浏览器窗口或标签页:
// 窗口尺寸
console.log(window.innerWidth); // 视口宽度(含滚动条)
console.log(window.innerHeight); // 视口高度(含滚动条)
console.log(window.outerWidth); // 浏览器窗口总宽度
console.log(window.outerHeight); // 浏览器窗口总高度
// 滚动位置
console.log(window.scrollX); // 水平滚动偏移(scrollX === pageXOffset)
console.log(window.scrollY); // 垂直滚动偏移(scrollY === pageYOffset)
// 滚动到指定位置(平滑滚动)
window.scrollTo({
top: 0,
behavior: 'smooth'
});
window.scrollBy({
top: 200, // 相对当前位置滚动
behavior: 'smooth'
});
// 滚动到指定元素
element.scrollIntoView({
behavior: 'smooth',
block: 'center', // 'start', 'center', 'end', 'nearest'
inline: 'nearest'
});
// 窗口操作
window.open('https://example.com', '_blank', 'width=800,height=600');
window.close(); // 仅能关闭通过 window.open 打开的窗口
// 定时器
const timeoutId = setTimeout(() => console.log('延迟执行'), 1000);
const intervalId = setInterval(() => console.log('定时执行'), 2000);
clearTimeout(timeoutId);
clearInterval(intervalId);
// requestAnimationFrame(在下次重绘前执行,约 60fps)
let animationId;
function animate() {
element.style.transform = `translateX(${pos}px)`;
pos += 2;
if (pos < 500) {
animationId = requestAnimationFrame(animate);
}
}
animationId = requestAnimationFrame(animate);
cancelAnimationFrame(animationId);6.2 navigator 对象
navigator 提供浏览器和操作系统信息:
// 浏览器信息
console.log(navigator.userAgent);
// "Mozilla/5.0 (Linux; ...) AppleWebKit/537.36 ..."
console.log(navigator.platform); // "Linux x86_64", "Win32", "MacIntel"
console.log(navigator.language); // "zh-CN"(浏览器语言)
console.log(navigator.languages); // ["zh-CN", "en"](用户偏好语言列表)
// 在线状态
console.log(navigator.onLine); // true / false
window.addEventListener('online', () => console.log('网络已连接'));
window.addEventListener('offline', () => console.log('网络已断开'));
// 硬件信息
console.log(navigator.hardwareConcurrency); // CPU 逻辑核心数
console.log(navigator.deviceMemory); // 设备内存(GB,约估值)
// 地理定位
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('纬度:', position.coords.latitude);
console.log('经度:', position.coords.longitude);
console.log('精度:', position.coords.accuracy);
},
(error) => {
console.error('定位失败:', error.message);
},
{ enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
);
// 持续监听位置
const watchId = navigator.geolocation.watchPosition(successCallback);
navigator.geolocation.clearWatch(watchId);
// 媒体设备(摄像头、麦克风)
async function getCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false
});
const video = document.querySelector('video');
video.srcObject = stream;
} catch (err) {
console.error('访问摄像头失败:', err);
}
}
// 枚举可用设备
async function listDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach(device => {
console.log(`${device.kind}: ${device.label} (${device.deviceId})`);
});
}
// 剪贴板 API(需要 HTTPS 或 localhost)
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('复制成功');
} catch (err) {
console.error('复制失败:', err);
}
}
async function readFromClipboard() {
try {
const text = await navigator.clipboard.readText();
console.log('剪贴板内容:', text);
} catch (err) {
console.error('读取剪贴板失败:', err);
}
}6.3 location 对象
location 对象管理当前页面的 URL:
// URL 分解
// 假设当前 URL: https://example.com:8080/path/to/page?q=js&page=1#section
console.log(location.href); // "https://example.com:8080/path/to/page?q=js&page=1#section"
console.log(location.protocol); // "https:"
console.log(location.host); // "example.com:8080"
console.log(location.hostname); // "example.com"
console.log(location.port); // "8080"
console.log(location.pathname); // "/path/to/page"
console.log(location.search); // "?q=js&page=1"
console.log(location.hash); // "#section"
console.log(location.origin); // "https://example.com:8080"
// URL 参数解析
function getQueryParams() {
const params = new URLSearchParams(location.search);
return Object.fromEntries(params.entries());
}
// { q: "js", page: "1" }
// 导航操作
location.reload(); // 重新加载当前页面
location.reload(true); // 强制从服务器重新加载(跳过缓存)
location.replace('https://example.com'); // 替换当前历史记录,不可后退
location.assign('https://example.com'); // 加载新页面,可后退
location.href = 'https://example.com'; // 类似 assign
// hash 变更监听
window.addEventListener('hashchange', () => {
console.log('Hash 变为:', location.hash);
// 可以用于 SPA 路由
handleRoute(location.hash);
});
// 使用 URL 构造函数解析 URL
function parseURL(url) {
const u = new URL(url);
console.log(u.hostname, u.pathname, u.searchParams.get('q'));
}6.4 history 对象
history 管理浏览器的会话历史(前进/后退):
// 基本导航
history.back(); // 后退一页
history.forward(); // 前进一页
history.go(-2); // 后退两页
history.go(1); // 前进一页
// 当前历史栈长度
console.log(history.length);
// HTML5 History API(SPA 路由核心)
// pushState — 添加新的历史记录条目
const state = { page: 'home', scrollY: window.scrollY };
history.pushState(state, '', '/home');
// replaceState — 替换当前历史记录条目
history.replaceState({ page: 'about' }, '', '/about');
// popstate 事件 — 用户点击前进/后退按钮时触发
window.addEventListener('popstate', (e) => {
console.log('状态变化:', e.state); // { page: 'home', ... }
// 根据 state 更新页面内容
if (e.state) {
loadPage(e.state.page);
}
});6.5 screen 对象
screen 提供用户屏幕的信息:
console.log(screen.width); // 屏幕总宽度(像素)
console.log(screen.height); // 屏幕总高度
console.log(screen.availWidth); // 可用宽度(排除任务栏等)
console.log(screen.availHeight); // 可用高度
console.log(screen.colorDepth); // 色彩深度(通常 24 或 32)
console.log(screen.orientation); // ScreenOrientation 对象
// 屏幕方向
if (screen.orientation) {
console.log(screen.orientation.type); // "landscape-primary", "portrait-primary"
// 锁定屏幕方向
screen.orientation.lock('portrait')
.then(() => console.log('锁定竖屏'))
.catch(err => console.error(err));
// 解除锁定
screen.orientation.unlock();
// 方向变化监听
screen.orientation.addEventListener('change', () => {
console.log('屏幕方向变为:', screen.orientation.type);
});
}
// 检测是否为 Retina 屏幕
const isRetina = window.devicePixelRatio >= 2;
console.log('设备像素比:', window.devicePixelRatio); // 1, 2, 36.6 localStorage / sessionStorage
Web Storage 提供客户端键值存储:
// localStorage — 持久化存储(不主动清除则永久保存)
// sessionStorage — 会话级存储(标签页关闭后清除)
// 公共 API(两者相同)
localStorage.setItem('key', 'value');
const val = localStorage.getItem('key');
localStorage.removeItem('key');
localStorage.clear(); // 清除所有
// 只能存储字符串,对象需序列化
const user = { id: 1, name: '张三', roles: ['admin'] };
localStorage.setItem('user', JSON.stringify(user));
const saved = JSON.parse(localStorage.getItem('user'));
console.log(saved.name); // "张三"
// 存储数值时自动转字符串
localStorage.setItem('count', 0);
console.log(typeof localStorage.getItem('count')); // "string"
// storage 事件(同源其他标签页修改 storage 时触发)
window.addEventListener('storage', (e) => {
console.log('键:', e.key);
console.log('旧值:', e.oldValue);
console.log('新值:', e.newValue);
console.log('来源:', e.url);
console.log('存储区域:', e.storageArea); // localStorage 对象
});
// 容量估算
function getStorageSize() {
let total = 0;
for (const key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += key.length + localStorage.getItem(key).length;
}
}
console.log(`已使用: ${(total / 1024).toFixed(2)} KB`);
}
// 封装带过期时间的存储
const storage = {
set(key, value, ttl = 0) { // ttl 毫秒
const data = { value, expiry: ttl ? Date.now() + ttl : 0 };
localStorage.setItem(key, JSON.stringify(data));
},
get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const data = JSON.parse(raw);
if (data.expiry && Date.now() > data.expiry) {
localStorage.removeItem(key);
return null;
}
return data.value;
}
};七、Intersection Observer
IntersectionObserver 异步观察目标元素与视口(或指定容器)的交叉状态,性能远优于 scroll 事件监听。
7.1 基本用法
// 创建观察器
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// entry 包含每个被观察元素的交叉信息
console.log('目标元素:', entry.target);
console.log('是否交叉:', entry.isIntersecting);
console.log('交叉比例:', entry.intersectionRatio);
console.log('交叉矩形:', entry.intersectionRect);
console.log('目标边界:', entry.boundingClientRect);
console.log('根边界:', entry.rootBounds);
});
}, {
root: null, // 默认为视口(浏览器可视区域)
rootMargin: '0px', // 扩大或缩小根边界
threshold: [0, 0.5, 1] // 触发回调的交叉比例阈值
});
// 开始观察
const target = document.getElementById('lazy-image');
observer.observe(target);
// 停止观察
observer.unobserve(target);
// 断开所有观察
observer.disconnect();7.2 懒加载图片实现
// HTML: <img class="lazy" data-src="https://example.com/image.jpg" alt="描述">
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 将 data-src 设置到 src 触发加载
img.src = img.dataset.src;
// 加载完成后移除 data-src 和懒加载类
img.addEventListener('load', () => {
img.classList.remove('lazy');
img.removeAttribute('data-src');
});
// 加载完成后停止观察
observer.unobserve(img);
}
});
}, {
rootMargin: '200px 0px', // 提前 200px 预加载
threshold: 0.01
});
// 观察所有懒加载图片
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
// 动态内容:新添加图片后重新观察
function addLazyImages(newImagesContainer) {
newImagesContainer.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
}7.3 无限滚动加载
class InfiniteScroll {
constructor(container, loadMoreCallback, options = {}) {
this.container = container;
this.loadMore = loadMoreCallback;
this.isLoading = false;
// 创建哨兵元素
this.sentinel = document.createElement('div');
this.sentinel.className = 'scroll-sentinel';
this.sentinel.style.height = '1px';
this.container.appendChild(this.sentinel);
// 配置 IntersectionObserver
this.observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !this.isLoading) {
this.load();
}
},
{
rootMargin: options.rootMargin || '100px',
threshold: 0
}
);
this.observer.observe(this.sentinel);
}
async load() {
this.isLoading = true;
this.showLoading();
try {
const items = await this.loadMore();
if (items.length === 0) {
this.noMoreData();
} else {
items.forEach(item => this.renderItem(item));
}
} catch (err) {
this.showError(err);
} finally {
this.isLoading = false;
this.hideLoading();
}
}
renderItem(item) {
const el = document.createElement('div');
el.className = 'item';
el.textContent = item.title;
this.sentinel.before(el);
}
showLoading() {
// 显示加载指示器
}
hideLoading() {
// 隐藏加载指示器
}
noMoreData() {
this.observer.unobserve(this.sentinel);
// 显示"没有更多数据"
}
showError(err) {
console.error('加载失败:', err);
}
destroy() {
this.observer.disconnect();
this.sentinel.remove();
}
}
// 使用
const scroll = new InfiniteScroll(
document.getElementById('feed'),
async () => {
const response = await fetch(`/api/items?page=${scroll.page++}`);
return response.json();
}
);7.4 曝光统计
class ExposureTracker {
constructor(options = {}) {
this.exposureData = new Map();
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target;
const data = this.exposureData.get(el);
if (data) {
this.reportExposure(el, data);
this.exposureData.delete(el);
this.observer.unobserve(el);
}
}
});
},
{
rootMargin: options.rootMargin || '0px',
threshold: options.threshold || 0.5
}
);
}
track(element, data) {
this.exposureData.set(element, data);
this.observer.observe(element);
}
reportExposure(element, data) {
// 上报曝光数据
const payload = {
elementId: element.id || element.dataset.trackId,
data,
timestamp: Date.now(),
viewportSize: {
width: window.innerWidth,
height: window.innerHeight
},
scrollPosition: {
x: window.scrollX,
y: window.scrollY
}
};
// 发送到分析服务器(使用 sendBeacon 确保页面关闭时也能发送)
navigator.sendBeacon('/api/exposure', JSON.stringify(payload));
console.log('曝光上报:', payload);
}
destroy() {
this.observer.disconnect();
this.exposureData.clear();
}
}
// 使用
const tracker = new ExposureTracker({ threshold: 0.5 });
document.querySelectorAll('[data-track]').forEach(el => {
tracker.track(el, {
type: el.dataset.track,
id: el.dataset.id
});
});八、MutationObserver
MutationObserver 监听 DOM 树的变化,是替代已弃用的 MutationEvent 的现代方案。
8.1 基本用法
// 创建观察器
const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach(mutation => {
console.log('变更类型:', mutation.type);
switch (mutation.type) {
case 'childList':
// 子节点增删
console.log('新增节点:', mutation.addedNodes);
console.log('移除节点:', mutation.removedNodes);
break;
case 'attributes':
// 属性变化
console.log('变化的属性:', mutation.attributeName);
console.log('旧值:', mutation.oldValue);
console.log('新值:', mutation.target.getAttribute(mutation.attributeName));
break;
case 'characterData':
// 文本内容变化
console.log('文本变化:', mutation.oldValue, '→', mutation.target.textContent);
break;
}
});
});
// 配置观察选项
const config = {
childList: true, // 监听子节点增删
attributes: true, // 监听属性变化
characterData: true, // 监听文本变化
subtree: true, // 监听所有后代节点(不仅是直接子节点)
attributeOldValue: true, // 记录属性旧值
characterDataOldValue: true, // 记录文本旧值
attributeFilter: ['class', 'style'] // 只监听指定的属性
};
// 开始观察
const targetNode = document.getElementById('container');
observer.observe(targetNode, config);
// 停止观察
observer.disconnect();
// 获取所有未处理的变更记录(不触发回调)
const records = observer.takeRecords();8.2 实际应用
// 1. 检测 DOM 元素是否被插入到页面
function waitForElement(selector, callback) {
const existing = document.querySelector(selector);
if (existing) {
callback(existing);
return;
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && node.matches(selector)) {
observer.disconnect();
callback(node);
return;
}
// 检查子节点
const child = node.nodeType === 1 && node.querySelector(selector);
if (child) {
observer.disconnect();
callback(child);
return;
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 超时处理
setTimeout(() => observer.disconnect(), 10000);
}
// 使用
waitForElement('#dynamic-content', (el) => {
console.log('元素已渲染:', el);
});
// 2. 内容变化后的自动保存
function autoSaveWhenContentChanges(formElement, saveCallback, delay = 1000) {
let saveTimer;
const observer = new MutationObserver(() => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
saveCallback(formElement);
}, delay);
});
observer.observe(formElement, {
childList: true,
subtree: true,
characterData: true
});
return () => observer.disconnect(); // 返回清理函数
}
const cleanup = autoSaveWhenContentChanges(
document.getElementById('editor'),
(form) => {
console.log('自动保存...');
localStorage.setItem('draft', form.innerHTML);
}
);
// 3. 监视频繁动态插入并去重
function trackDynamicImages() {
const processed = new WeakSet();
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
node.querySelectorAll('img').forEach(img => {
if (!processed.has(img)) {
processed.add(img);
console.log('新图片加载:', img.src);
img.addEventListener('error', () => {
console.error('图片加载失败:', img.src);
});
}
});
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
return observer;
}九、ResizeObserver
ResizeObserver 观察元素尺寸变化,比 window.resize 事件更精确。
9.1 基本用法
// 创建观察器
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { target, contentRect, borderBoxSize, contentBoxSize } = entry;
console.log('目标元素:', target);
console.log('内容矩形:', contentRect);
// contentRect: { x, y, width, height, top, right, bottom, left }
// 新版 API:获取精确的像素值
if (contentBoxSize) {
const inlineSize = contentBoxSize[0].inlineSize; // 宽度
const blockSize = contentBoxSize[0].blockSize; // 高度
console.log(`尺寸: ${inlineSize} x ${blockSize}`);
}
});
});
// 开始观察
const element = document.getElementById('resizable');
observer.observe(element);
// 停止观察
observer.unobserve(element);
// 断开所有
observer.disconnect();9.2 响应式组件
// 1. 根据容器宽度调整布局
class ResponsiveGrid {
constructor(container) {
this.container = container;
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
this.handleResize(entry);
}
});
this.resizeObserver.observe(container);
}
handleResize(entry) {
const { width } = entry.contentRect;
// 根据容器宽度调整列数
let columns;
if (width < 400) {
columns = 1;
} else if (width < 800) {
columns = 2;
} else if (width < 1200) {
columns = 3;
} else {
columns = 4;
}
this.container.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
this.container.dataset.columns = columns;
}
destroy() {
this.resizeObserver.disconnect();
}
}
// 使用
const grid = new ResponsiveGrid(document.querySelector('.grid-container'));
// 2. 自适应字号(让文本填满容器)
function autoFontSize(element, maxSize = 72, minSize = 12) {
const observer = new ResizeObserver(() => {
let fontSize = maxSize;
element.style.fontSize = `${fontSize}px`;
while (element.scrollWidth > element.clientWidth && fontSize > minSize) {
fontSize -= 1;
element.style.fontSize = `${fontSize}px`;
}
});
observer.observe(element);
return observer;
}
// 3. 文本域自动高度
function autoResizeTextarea(textarea) {
const observer = new ResizeObserver(() => {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
});
textarea.addEventListener('input', () => {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
});
observer.observe(textarea);
return observer;
}
// 4. 跟踪所有元素尺寸用于分析
function trackElementSizes(selector) {
const data = new Map();
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
data.set(entry.target, {
width: entry.contentRect.width,
height: entry.contentRect.height,
timestamp: Date.now()
});
});
});
document.querySelectorAll(selector).forEach(el => observer.observe(el));
return { observer, data };
}9.3 ResizeObserver 与 window.resize 对比
| 特性 | ResizeObserver | window.resize |
|---|---|---|
| 精度 | 监听单个元素尺寸变化 | 仅监听窗口变化 |
| 性能 | 高效,批量处理 | 频繁触发 |
| 应用场景 | 容器查询、响应式组件 | 全局布局调整 |
| 是否支持 iframe | ✅ | ❌ |
十、DOM 性能优化
10.1 重排(Reflow)与重绘(Repaint)
重排(Reflow/Layout):浏览器重新计算元素的位置和几何尺寸。代价高昂。
重绘(Repaint):元素视觉样式改变但布局不变时的重新绘制。代价较低。
// 触发重排的操作
// 1. 修改几何属性
element.style.width = '200px';
element.style.height = '100px';
element.style.padding = '20px';
element.style.margin = '10px';
element.style.borderWidth = '2px';
// 2. 修改 DOM 树
element.appendChild(newChild);
element.removeChild(child);
element.innerHTML = '<div>新内容</div>';
// 3. 修改类名(如果类名影响布局)
element.className = 'new-layout';
// 4. 访问布局属性(强制同步布局)
const height = element.offsetHeight; // 强制触发重排
const width = element.offsetWidth; // 强制触发重排
const rect = element.getBoundingClientRect(); // 强制触发重排
const computed = getComputedStyle(element).height; // 强制触发重排
// 仅触发重绘(不改变布局)
element.style.color = 'red';
element.style.backgroundColor = '#eee';
element.style.visibility = 'hidden'; // display:none 会触发重排
element.style.borderColor = 'blue';10.2 批量 DOM 操作
// ✅ 策略1:使用 DocumentFragment(已介绍)
// ✅ 策略2:使用 class 批量更新样式
// ❌ 低效:逐条修改样式(触发多次重排)
element.style.fontSize = '18px';
element.style.padding = '10px';
element.style.margin = '5px';
element.style.border = '1px solid #ccc';
element.style.color = '#333';
// ✅ 高效:使用 class 一次性应用
element.className = 'card card--active';
// OR: 使用 cssText
element.style.cssText = 'font-size: 18px; padding: 10px; color: #333;';
// OR: 使用 classList 结合 toggle 的第二个参数
element.classList.toggle('active', true);
// ✅ 策略3:批量读取,批量写入(分离读写操作)
// ❌ 错误的读写交替(强制多次重排)
for (let i = 0; i < items.length; i++) {
items[i].style.height = items[i].offsetWidth + 'px'; // 读 + 写
// 每次循环都先读取 offsetWidth(触发重排),再写 height
}
// ✅ 正确的做法:先批量读取,再批量写入
const widths = [];
for (let i = 0; i < items.length; i++) {
widths.push(items[i].offsetWidth); // 读取阶段(触发一次重排)
}
for (let i = 0; i < items.length; i++) {
items[i].style.height = widths[i] + 'px'; // 写入阶段
}10.3 离线 DOM 操作
将元素从文档流中移除进行操作,完成后放回:
// 方法1:display: none
function updateCard(cardSelector) {
const card = document.querySelector(cardSelector);
// 1. 从渲染树中移除
card.style.display = 'none';
// 2. 在内存中执行多次操作(不会触发重排)
card.innerHTML = '<div class="header">...</div><div class="body">...</div>';
card.classList.add('updated');
card.style.padding = '20px';
card.dataset.version = '2.0';
// 3. 重新显示(触发一次重排)
card.style.display = '';
}
// 方法2:克隆 + 替换
function updateCardV2(cardSelector) {
const card = document.querySelector(cardSelector);
// 1. 深克隆(复制当前样式和结构到内存中)
const clone = card.cloneNode(true);
// 2. 在克隆体上做所有操作
clone.innerHTML = '<div class="header">...</div><div class="body">...</div>';
clone.classList.add('updated');
clone.style.padding = '20px';
// 3. 替换原节点(仅触发一次重排)
card.replaceWith(clone);
}
// 方法3:DocumentFragment(前面已介绍)10.4 requestAnimationFrame 进行 DOM 更新
requestAnimationFrame(rAF)在浏览器下一次重绘之前执行回调,是动画和批量 DOM 更新的最佳选择。
// 1. 基本动画循环
function startAnimation(element) {
let position = 0;
function animate(timestamp) {
// timestamp 是 rAF 传入的高精度时间戳(毫秒)
position += 2;
element.style.transform = `translateX(${position}px)`;
if (position < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// 2. 使用 rAF 批量处理 DOM 更新
const updateQueue = [];
let isScheduled = false;
function scheduleDOMUpdate(callback) {
updateQueue.push(callback);
if (!isScheduled) {
isScheduled = true;
requestAnimationFrame(() => {
// 在一次 rAF 中批量执行所有更新
const start = performance.now();
while (updateQueue.length > 0) {
const cb = updateQueue.shift();
cb();
}
console.log(`批量更新耗时: ${performance.now() - start}ms`);
isScheduled = false;
});
}
}
// 使用
scheduleDOMUpdate(() => {
element1.classList.add('visible');
});
scheduleDOMUpdate(() => {
element2.textContent = '更新文本';
});
// 3. rAF 防抖(代替 setTimeout 做视觉更新)
function onScroll() {
// ❌ setTimeout 不可靠
// setTimeout(updateUI, 16);
// ✅ 使用 rAF,在重绘前只执行一次
if (!rafId) {
rafId = requestAnimationFrame(() => {
updateUI();
rafId = null;
});
}
}
window.addEventListener('scroll', onScroll, { passive: true });10.5 完整优化清单
// DOM 性能优化清单
// 1. 最小化 DOM 访问次数 —— 缓存引用
// ❌
for (let i = 0; i < 100; i++) {
document.getElementById('list').appendChild(createItem());
}
// ✅
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
fragment.appendChild(createItem());
}
list.appendChild(fragment);
// 2. 避免强制同步布局
// ❌
function setWidth() {
// forEach 写在读取后面
const widths = elements.map(el => el.offsetWidth);
elements.forEach((el, i) => {
el.style.width = widths[i] * 2 + 'px';
});
}
// 3. 使用 transform 和 opacity 做动画
// ❌ 修改 left/top 触发重排
element.style.left = '100px';
element.style.top = '200px';
// ✅ 使用 transform(仅触发合成,不触发重排重绘)
element.style.transform = 'translate(100px, 200px)';
// 4. 使用 will-change 提示浏览器优化
element.style.willChange = 'transform';
// 使用完后记得移除,避免过度占用 GPU 内存
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});
// 5. 事件委托减少监听器
// 6. 使用 IntersectionObserver 替代 scroll 事件
// 7. 使用 ResizeObserver 替代 window.resize
// 8. 使用 DocumentFragment 批量插入
// 9. 读写分离,批量操作
// 10. 使用虚拟 DOM 减少直接 DOM 操作(React/Vue 场景)总结
本文全面介绍了 JavaScript DOM 和 BOM 的核心知识体系:
- DOM 基础:涵盖了 DOM 树结构、Node 与 Element 接口的区别、节点导航的两种模式、五种核心查询方法及其实时/静态集合特性。
- 节点操作:从创建、插入、替换到删除,对比了传统方法与现代 API(
append、prepend、after、before)的差异;重点介绍了DocumentFragment的批量操作优化。 - 属性与内容:深入剖析了
innerHTML、outerHTML、textContent、innerText四者的差异,以及dataset、classList的现代用法。 - 样式操作:覆盖行内样式、计算样式、CSS 变量的 JS 读写,以及
className与classList的适用场景。 - 事件机制:完整解释了事件流的三个阶段、
addEventListener的高级选项(once、passive、signal)、事件委托模式、自定义事件及常见事件分类。 - BOM:系统介绍了
window、navigator、location、history、screen以及 Web Storage 的完整 API,涵盖地理定位、媒体设备、剪贴板等实用能力。 - 三大 Observer:
IntersectionObserver(懒加载/无限滚动/曝光统计)、MutationObserver(DOM 变化监听)、ResizeObserver(元素尺寸响应),每个都配有可直接运行的实战代码。 - 性能优化:从重排重绘的原理出发,总结了批量操作、离线 DOM、读写分离、
requestAnimationFrame等关键优化策略。
掌握这些知识,你将能够高效地操纵 DOM、利用浏览器能力、编写高性能的前端交互代码。