JavaScript 事件处理与 DOM 操作:从基础到高级实践
一、DOM 基础与核心概念
1. DOM 树结构解析
DOM(文档对象模型)是 HTML 和 XML 文档的编程接口,它将文档表示为节点树:
<!DOCTYPE html>
<html>
<head>
<title>DOM 示例</title>
</head>
<body>
<h1>欢迎学习DOM</h1>
<p class="content">这是一个段落</p>
</body>
</html>
对应的 DOM 树结构:
document
├─ html
├─ head
│ └─ title
│ └─ "DOM 示例"
└─ body
├─ h1
│ └─ "欢迎学习DOM"
└─ p
└─ "这是一个段落"
2. 节点类型详解
| 节点类型 | nodeType | 示例 |
|---|---|---|
| 元素节点 (Element) | 1 | <div> <p> <span> |
| 属性节点 (Attribute) | 2 | class="example" |
| 文本节点 (Text) | 3 | "Hello World" |
| 注释节点 (Comment) | 8 | <!--- 注释 ----> |
| 文档节点 (Document) | 9 | document |
| 文档类型节点 (DocType) | 10 | < !DOCTYPE html> |
| 轻量级文档片段 (DocumentFragment) | 11 | 不在主 DOM 中渲染,操作不会触发重排/重绘 |
3. DOM 查询方法
1. 经典查询API
// 获取单个元素
document.getElementById('header');
// 获取元素集合(动态)
const divs = document.getElementsByTagName('div');
// 获取类名元素集合(动态)
const items = document.getElementsByClassName('item');
2. 现代选择器API
// 查询单个元素
document.querySelector('#main .article:first-child');
// 查询所有匹配元素(静态)
document.querySelectorAll('button[data-action]');
3. 节点关系导航
| 属性 | 描述 |
|---|---|
parentNode | 父节点 |
childNodes | 所有子节点(包含文本节点) |
children | 仅元素子节点 |
firstChild/lastChild | 首/末子节点(含文本节点) |
firstElementChild | 首个子元素节点 |
nextSibling | 下一个兄弟节点(含文本节点) |
previousElementSibling | 上一个元素兄弟节点 |
// 父节点
const parent = element.parentNode;
// 子节点
const firstChild = element.firstChild; // 包含文本节点
const firstElementChild = element.children[0]; // 仅元素节点
// 兄弟节点
const nextSibling = element.nextSibling; // 包含文本节点
const nextElementSibling = element.nextElementSibling; // 仅元素节点
// 子节点列表
const children = element.children; // HTMLCollection (仅元素节点)
const childNodes = element.childNodes; // NodeList (所有类型节点)
4. DOM 操作方法
1. 内容操作方法
-
文本内容
// 获取/设置文本(忽略HTML标签) element.textContent = "新文本内容"; const text = element.textContent; -
HTML内容
// 获取/设置包含HTML的内容 element.innerHTML = "<strong>加粗文本</strong>"; const htmlContent = element.innerHTML; -
表单值
// 获取/设置表单元素值 input.value = "新值"; const inputValue = input.value; -
拓展
textContent 与 innertext 区别:
| 特性 | textContent | innerText |
|---|---|---|
| 标准 | DOM Level 1 (所有浏览器支持) | 非标准(但现代浏览器已实现) |
| 获取内容 | 返回所有文本(包括隐藏元素) | 返回「可见文本」(忽略隐藏元素) |
| 性能 | 更高(不触发重排计算) | 较低(需计算布局) |
| 换行处理 | 保留源码中的换行和空格 | 合并空白符,按渲染结果返回 |
实际行为对比
示例代码:
<div id="example">
Hello World!
<span style="display:none">Hidden Text</span>
<p>Another Line</p>
</div>
const div = document.getElementById('example');
console.log(div.textContent);
// 输出(保留所有内容,包括隐藏元素和格式):
// "Hello World!
// Hidden Text
// Another Line"
console.log(div.innerText);
// 输出(仅可见文本,合并空白符):
// "Hello World!
// Another Line"
2.属性操作方法
// 获取属性
const href = link.getAttribute('href');
// 设置属性
img.setAttribute('src', 'new-image.jpg');
// 移除属性
div.removeAttribute('data-temp');
// class 操作(推荐)
div.classList.add('active'); // 添加类
div.classList.remove('old'); // 移除类
div.classList.toggle('hidden'); // 切换类
div.classList.contains('active'); // 检查类
3. 样式操作方法
1.单个样式属性修改
// 语法:element.style.property = value
const div = document.querySelector('div');
// 设置样式(注意驼峰命名)
div.style.color = 'red';
div.style.backgroundColor = '#f0f0f0';
div.style.fontSize = '16px'; // 不是 font-size
div.style.marginTop = '10px'; // 不是 margin-top
// 获取样式(只能获取内联样式)
const color = div.style.color; // 仅返回通过JS设置或HTML内联的样式
- 批量设置样式
// 方法1:cssText(会覆盖全部内联样式)
element.style.cssText = 'color: red; background: yellow; font-size: 16px;';
// 方法2:setAttribute
element.setAttribute('style', 'color: red; background: yellow;');
//setAttribute('style', ...) 会重写整个 style 属性,导致重绘
拓展
style.setProperty()只修改单个样式属性,性能更好setAttribute('style', ...)会重写整个 style 属性,导致重绘
| 特性 | element.style.setProperty() | element.setAttribute() |
|---|---|---|
| 作用对象 | 专门操作元素的CSS 样式属性 | 操作元素的任意 HTML 属性 |
| 所属 API | CSSStyleDeclaration 接口 | Element 接口 |
| CSS 属性名格式 | 使用 CSS 标准命名 (background-color) | 使用 DOM 属性命名 (backgroundColor) |
| 优先级 | 修改内联样式(高优先级) | 修改 HTML 属性(不影响计算样式优先级) |
| 适用场景 | 动态修改样式 | 修改/添加任意 HTML 属性 |
// 方法1:使用 style.setProperty (推荐)
div.style.setProperty('background-color', 'red');
div.style.setProperty('font-size', '16px', 'important');
// 方法2:使用 setAttribute
div.setAttribute('style', 'background-color: red; font-size: 16px;');
3.设置class类名
className 属性(直接操作类名字符串)
// 完全替换所有类
element.className = 'active box highlighted';
// 追加类(需手动处理字符串)
element.className += ' new-class';
classList API(推荐方式)
const div = document.querySelector('div');
// 添加类
div.classList.add('active', 'highlight'); // 可一次添加多个
// 移除类
div.classList.remove('old-class');
// 切换类(无则添加,有则移除)
div.classList.toggle('hidden');
// 检查类是否存在
if (div.classList.contains('active')) {
// 执行操作
}
// 替换类
div.classList.replace('old-class', 'new-class');
4.获取计算样式
// 获取元素最终计算样式(包括内联、样式表和浏览器默认样式)
const style = window.getComputedStyle(element);
// 获取特定属性值
const width = style.getPropertyValue('width');
const fontSize = style.fontSize;
// 注意:返回的值是计算后的绝对值
// 如 margin 返回 "10px" 而不是百分比或 em
4. 元素创建与操作
// 创建新元素
const newDiv = document.createElement('div');
newDiv.className = "box";
// 添加文本节点
const textNode = document.createTextNode("Hello World");
newDiv.appendChild(textNode);
// 插入元素
parent.appendChild(newDiv); // 末尾插入
parent.insertBefore(newDiv, reference); // 指定位置插入
// 克隆元素
const clonedElement = original.cloneNode(true); // true 表示深克隆
// 替换元素
parent.replaceChild(newElement, oldElement);
// 删除元素
parent.removeChild(element); // 传统方法
element.remove(); // 现代方法(推荐)
| 特性 | element.append() | element.appendChild() |
|---|---|---|
| 参数类型 | 接受多个节点或字符串 | 只接受单个节点 |
| 返回值 | 无返回值 (undefined) | 返回追加的节点 |
| 字符串处理 | 自动将字符串转为文本节点 | 不接受字符串参数 |
| 浏览器支持 | 较新方法 (IE 不支持) | 所有浏览器支持 |
| 链式调用 | 不支持 (无返回值) | 支持 (返回节点可继续操作) |
| 特性 | replaceChild | replaceWith | removeChild | remove |
|---|---|---|---|---|
| 调用对象 | 父节点 | 要被替换的节点 | 父节点 | 要被移除的节点 |
| 参数 | (新节点, 旧节点) | 多个节点或字符串 | 要移除的子节点 | 无 |
| 返回值 | 被替换的旧节点 | undefined | 被移除的节点 | undefined |
| 字符串支持 | 不支持 | 支持 | 不适用 | 不适用 |
| 浏览器支持 | 所有浏览器 | IE不支持 | 所有浏览器 | IE不支持 |
| 链式调用 | 可以(返回旧节点) | 不可以 | 可以(返回被移除节点) | 不可以 |
// 创建新元素
const newDiv = document.createElement('div');
newDiv.className = 'alert';
newDiv.innerHTML = '<strong>提示!</strong> 重要消息';
// 添加样式
newDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
background: #ffeb3b;
border-radius: 4px;
`;
// 添加属性
newDiv.setAttribute('role', 'alert');
newDiv.dataset.type = 'warning'; // 自定义数据属性
// 添加到DOM
document.body.append(newDiv);
// 克隆元素
const clonedDiv = newDiv.cloneNode(true); // 深度克隆
// 替换元素
const oldDiv = document.querySelector('.old');
oldDiv.replaceWith(newDiv);
// 移除元素
newDiv.remove();
二、事件处理机制深度解析
1. 事件流:捕获与冒泡
<div id="grandparent">
<div id="parent">
<button id="child">点击我</button>
</div>
</div>
const elements = ['grandparent', 'parent', 'child'].map(id => document.getElementById(id));
// 捕获阶段 (从外向内)
elements.forEach(el => {
el.addEventListener('click', () => {
console.log(`捕获: ${el.id}`);
}, true); // 第三个参数为true表示捕获阶段
});
// 冒泡阶段 (从内向外)
elements.forEach(el => {
el.addEventListener('click', () => {
console.log(`冒泡: ${el.id}`);
}); // 默认冒泡阶段
});
点击按钮后的输出顺序:
捕获: grandparent
捕获: parent
捕获: child
冒泡: child
冒泡: parent
冒泡: grandparent
2. 事件对象详解
事件处理函数接收的事件对象包含丰富属性和方法:
document.getElementById('myBtn').addEventListener('click', function(event) {
console.log('事件类型:', event.type); // "click"
console.log('目标元素:', event.target); // 实际触发元素
console.log('当前元素:', event.currentTarget); // 处理事件的元素
console.log('事件阶段:', event.eventPhase); // 2 (目标阶段)
console.log('时间戳:', event.timeStamp);
console.log('坐标:', {x: event.clientX, y: event.clientY});
// 阻止默认行为
event.preventDefault();
// 停止冒泡
event.stopPropagation();
// 立即停止其他监听器执行
event.stopImmediatePropagation();
});
3. 常用事件类型分类
| 事件类别 | 常见事件 |
|---|---|
| 鼠标事件 | click, dblclick, mouseenter, mouseleave, mousemove, contextmenu |
| 键盘事件 | keydown, keyup, keypress |
| 表单事件 | submit, change, input, focus, blur, reset |
| 窗口事件 | load, resize, scroll, hashchange, beforeunload |
| 触摸事件 | touchstart, touchmove, touchend |
| 多媒体事件 | play, pause, volumechange, ended |
| 剪切板事件 | copy, cut, paste |
4.事件注册
传统 HTML 事件属性(不推荐)
<button onclick="handleClick()">点击</button>
<script>
function handleClick() {
console.log("按钮被点击");
}
</script>
缺点:
- HTML 与 JavaScript 强耦合
- 只能绑定一个处理函数
- 不易维护
** DOM 属性事件绑定**
const btn = document.querySelector("button");
btn.onclick = function() {
console.log("第一次点击");
};
// 会覆盖之前的事件
btn.onclick = function() {
console.log("第二次点击");
};
缺点:
- 同类型事件只能绑定一个处理函数
- 容易被覆盖
** addEventListener(推荐)**
const btn = document.querySelector("button");
function firstClick() {
console.log("第一次点击");
}
function secondClick() {
console.log("第二次点击");
}
// 可以绑定多个事件
btn.addEventListener("click", firstClick);
btn.addEventListener("click", secondClick,{
capture: false, // 是否捕获阶段触发
once: true, // 只执行一次
passive: true // 声明不会调用preventDefault()
});
事件移除
// 必须引用同一个函数
btn.removeEventListener("click", firstClick);
一次性事件
btn.addEventListener("click", function() {
console.log("只触发一次");
}, { once: true });
// 或
function handleOnce(){
console.log("只执行一次 ");
btn.removeEventListener('click', handleOnce)
}
btn.addEventListener('click', handleOnce)
三、高级 DOM 操作技巧
1. 元素创建与修改
// 创建新元素
const newDiv = document.createElement('div');
newDiv.className = 'alert';
newDiv.innerHTML = '<strong>提示!</strong> 重要消息';
// 添加样式
newDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
background: #ffeb3b;
border-radius: 4px;
`;
// 添加属性
newDiv.setAttribute('role', 'alert');
newDiv.dataset.type = 'warning'; // 自定义数据属性
// 添加到DOM
document.body.append(newDiv);
// 克隆元素
const clonedDiv = newDiv.cloneNode(true); // 深度克隆
// 替换元素
const oldDiv = document.querySelector('.old');
oldDiv.replaceWith(newDiv);
// 移除元素
newDiv.remove();
2. 性能优化技巧
// 1. 批量DOM修改使用文档片段 -- document.createDocumentFragment创建的元素在浏览器中不显示,类似于<><>
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('div');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
document.getElementById('container').appendChild(fragment);
// 2. 使用requestAnimationFrame优化动画
function animate() {
const element = document.getElementById('animated');
let pos = 0;
function frame() {
if (pos === 300) {
cancelAnimationFrame(animationId);
} else {
pos++;
element.style.left = pos + 'px';
animationId = requestAnimationFrame(frame);
}
}
let animationId = requestAnimationFrame(frame);
}
// 3. 事件委托减少监听器数量
document.getElementById('list').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('点击了:', event.target.textContent);
}
});
// 4. 使用IntersectionObserver实现懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('img.lazy').forEach(img => {
observer.observe(img);
});
四、现代事件处理模式
1. 自定义事件系统
// 创建自定义事件
const myEvent = new CustomEvent('myevent', {
detail: { message: '自定义事件数据' },
bubbles: true,
cancelable: true
});
// 监听自定义事件
document.addEventListener('myevent', (e) => {
console.log('收到自定义事件:', e.detail.message);
});
// 触发事件
document.dispatchEvent(myEvent);
// 元素特定事件
const button = document.getElementById('myButton');
button.addEventListener('specialClick', (e) => {
console.log('特殊点击:', e.detail);
});
button.dispatchEvent(new CustomEvent('specialClick', {
detail: { count: 5 }
}));
2. 事件委托高级模式
// 动态过滤委托
document.getElementById('gallery').addEventListener('click', function(e) {
// 查找最近的匹配元素
const thumbnail = e.target.closest('.thumbnail');
if (!thumbnail) return;
if (e.ctrlKey) {
thumbnail.classList.toggle('selected');
} else {
showFullImage(thumbnail.dataset.imageId);
}
});
// 多事件类型委托
const eventTypes = ['click', 'touchstart'];
const handler = function(e) {
console.log(`${e.type}事件在`, e.target);
};
eventTypes.forEach(type => {
document.getElementById('controls').addEventListener(type, handler);
});
// 移除多个事件
function removeEvents() {
eventTypes.forEach(type => {
document.getElementById('controls').removeEventListener(type, handler);
});
}
3. Promise 与事件结合
function waitForEvent(element, eventType) {
return new Promise((resolve) => {
const listener = (e) => {
element.removeEventListener(eventType, listener);
resolve(e);
};
element.addEventListener(eventType, listener);
});
}
// 使用示例
async function handleButton() {
const button = document.getElementById('asyncBtn');
button.textContent = '等待点击...';
const event = await waitForEvent(button, 'click');
console.log('按钮被点击了!', event);
button.textContent = '已点击!';
setTimeout(() => {
button.textContent = '点击我';
}, 2000);
}
五、实战应用案例
1. 可拖拽排序列表
class DraggableList {
constructor(listElement) {
this.list = listElement;
this.setupEvents();
}
setupEvents() {
this.list.addEventListener('mousedown', this.handleMouseDown.bind(this));
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
// 触摸支持
this.list.addEventListener('touchstart', this.handleTouchStart.bind(this));
document.addEventListener('touchmove', this.handleTouchMove.bind(this));
document.addEventListener('touchend', this.handleTouchEnd.bind(this));
}
handleMouseDown(e) {
if (!e.target.classList.contains('draggable')) return;
this.draggedItem = e.target;
this.startY = e.clientY;
this.draggedItem.classList.add('dragging');
// 创建占位符
this.placeholder = document.createElement('div');
this.placeholder.className = 'placeholder';
this.draggedItem.parentNode.insertBefore(
this.placeholder,
this.draggedItem.nextSibling
);
}
handleMouseMove(e) {
if (!this.draggedItem) return;
e.preventDefault();
const y = e.clientY;
const deltaY = y - this.startY;
// 移动元素
this.draggedItem.style.transform = `translateY(${deltaY}px)`;
// 检查是否需要交换位置
const items = [...this.list.children];
const draggedIndex = items.indexOf(this.draggedItem);
const overIndex = items.findIndex(item => {
if (item === this.draggedItem || item === this.placeholder) return false;
const rect = item.getBoundingClientRect();
return y > rect.top + rect.height / 2;
});
if (overIndex !== -1 && overIndex !== draggedIndex) {
if (overIndex > draggedIndex) {
this.list.insertBefore(this.placeholder, items[overIndex + 1]);
} else {
this.list.insertBefore(this.placeholder, items[overIndex]);
}
}
}
handleMouseUp() {
if (!this.draggedItem) return;
// 放置元素到占位符位置
this.list.insertBefore(this.draggedItem, this.placeholder);
this.placeholder.remove();
this.draggedItem.style.transform = '';
this.draggedItem.classList.remove('dragging');
this.draggedItem = null;
this.placeholder = null;
}
// 触摸事件处理 (类似鼠标事件)
handleTouchStart(e) {
this.handleMouseDown({
target: e.target,
clientY: e.touches[0].clientY,
preventDefault: () => e.preventDefault()
});
}
handleTouchMove(e) {
this.handleMouseMove({
clientY: e.touches[0].clientY,
preventDefault: () => e.preventDefault()
});
}
handleTouchEnd() {
this.handleMouseUp();
}
}
// 使用
new DraggableList(document.getElementById('sortable-list'));
2. 表单实时验证系统
class FormValidator {
constructor(form, config) {
this.form = form;
this.config = config;
this.errors = {};
this.init();
}
init() {
// 设置初始状态
Object.keys(this.config.fields).forEach(field => {
this.errors[field] = [];
const input = this.form.querySelector(`[name="${field}"]`);
this.setupFieldValidation(input, field);
});
// 表单提交验证
this.form.addEventListener('submit', (e) => {
if (!this.validateAll()) {
e.preventDefault();
this.showGlobalErrors();
}
});
}
setupFieldValidation(input, field) {
const rules = this.config.fields[field];
// 实时验证
input.addEventListener('input', () => {
this.validateField(input, field, rules);
});
// 失去焦点验证
input.addEventListener('blur', () => {
this.validateField(input, field, rules, true);
});
}
validateField(input, field, rules, showErrors = false) {
const value = input.value.trim();
this.errors[field] = [];
// 必填验证
if (rules.required && !value) {
this.errors[field].push(this.config.messages.required);
}
// 正则验证
if (value && rules.pattern && !rules.pattern.test(value)) {
this.errors[field].push(this.config.messages.pattern);
}
// 自定义验证
if (value && rules.validate) {
const customError = rules.validate(value, this.form);
if (customError) {
this.errors[field].push(customError);
}
}
// 更新UI
this.updateFieldStatus(input, field, showErrors);
}
updateFieldStatus(input, field, showErrors) {
const errorContainer = input.nextElementSibling;
if (this.errors[field].length > 0) {
input.classList.add('invalid');
if (showErrors) {
errorContainer.textContent = this.errors[field][0];
errorContainer.style.display = 'block';
}
} else {
input.classList.remove('invalid');
input.classList.add('valid');
errorContainer.style.display = 'none';
}
}
validateAll() {
let isValid = true;
Object.keys(this.config.fields).forEach(field => {
const input = this.form.querySelector(`[name="${field}"]`);
const rules = this.config.fields[field];
this.validateField(input, field, rules, true);
if (this.errors[field].length > 0) {
isValid = false;
}
});
return isValid;
}
showGlobalErrors() {
const globalError = this.form.querySelector('.global-error');
if (!globalError) return;
const errorCount = Object.values(this.errors)
.reduce((sum, errors) => sum + errors.length, 0);
if (errorCount > 0) {
globalError.textContent = this.config.messages.global;
globalError.style.display = 'block';
} else {
globalError.style.display = 'none';
}
}
}
// 使用示例
const validator = new FormValidator(document.getElementById('signup-form'), {
fields: {
username: {
required: true,
pattern: /^[a-zA-Z0-9_]{4,16}$/,
validate: (value) => {
if (value.toLowerCase() === 'admin') {
return '不能使用保留用户名';
}
}
},
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
password: {
required: true,
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/
}
},
messages: {
required: '必填字段',
pattern: '格式不正确',
global: '请修正表单中的错误'
}
});
六、性能优化与最佳实践
1. 事件处理优化策略
| 策略 | 说明 | 代码示例 |
|---|---|---|
| 事件委托 | 减少事件监听器数量 | container.addEventListener('click', (e) => { if(e.target.matches('.btn')) {...} }); |
| 被动事件监听 | 提高滚动性能,告诉浏览器不会调用preventDefault() | window.addEventListener('scroll', onScroll, { passive: true }); |
| 防抖与节流 | 控制事件触发频率 | 见下方详细实现 |
| 及时移除监听器 | 避免内存泄漏 | element.removeEventListener('click', handler); |
| requestIdleCallback | 在浏览器空闲时处理非关键任务 | requestIdleCallback(() => { processLowPriorityData(); }); |
2. 防抖与节流实现
// 防抖 (最后一次操作后等待一段时间执行)
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流 (固定时间间隔执行)
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用示例
window.addEventListener('resize', debounce(() => {
console.log('调整窗口大小结束');
}, 300));
document.addEventListener('mousemove', throttle((e) => {
console.log('鼠标位置:', e.clientX, e.clientY);
}, 100));
3. DOM 操作优化建议
-
批量读取/写入:避免布局抖动
// 不好 - 导致多次重排 element.style.width = '100px'; element.style.height = '200px'; // 好 - 使用cssText或class element.style.cssText = 'width:100px; height:200px;'; // 或 element.classList.add('new-size'); -
使用文档片段:减少重绘次数
DocumentFragment 是一个轻量级的文档对象,它不属于主 DOM 树的一部分,但可以像普通 DOM 一样存储和操作节点。它是批量 DOM 操作时的性能优化利器。
** 核心特性**
- 内存中的文档片段:不在主 DOM 中渲染,操作不会触发重排/重绘
- 批量插入节点:一次性将多个节点添加到 DOM,减少页面回流
- 临时容器:作为节点操作的临时工作区
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('div');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
document.body.appendChild(fragment);
-
缓存DOM查询结果:避免重复查询
// 不好 for (let i = 0; i < 10; i++) { document.getElementById('list').appendChild(createItem(i)); } // 好 const list = document.getElementById('list'); for (let i = 0; i < 10; i++) { list.appendChild(createItem(i)); } -
使用虚拟DOM:对于复杂UI
// 使用React等库或小型虚拟DOM实现 function updateDOM(newVTree, oldVTree, parent) { // 比较虚拟DOM差异并最小化更新 }
七、现代浏览器 API 进阶
1. MutationObserver
用于异步监测DOM树的变化
// 1. 创建观察者
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
console.log('子节点变化:', mutation.addedNodes, mutation.removedNodes);
} else if (mutation.type === 'attributes') {
console.log(`属性 ${mutation.attributeName} 变化`);
}
});
});
// 2. 配置观察选项
observer.observe(document.getElementById('app'), {
attributes: true, // 观察属性变化
childList: true, // 观察子节点变化
subtree: true, // 观察所有后代节点
attributeOldValue: true, // 记录属性旧值
characterData: true // 观察文本内容变化
});
// 3. 停止观察
// observer.disconnect();
2. IntersectionObserver
IntersectionObserver 是现代浏览器提供的 API,用于高效监测目标元素是否进入或离开视口(Viewport)或某个父容器,替代传统的滚动事件监听 + getBoundingClientRect() 计算方式。
基本用法
1.创建 IntersectionObserver
const observer = new IntersectionObserver(callback, options);
callback:当目标元素进入或离开视口时触发的回调函数。options(可选):配置观察行为。
每个 entry 包含以下关键信息:
| 属性 | 类型 | 说明 |
|---|---|---|
target | Element | 被观察的目标元素 |
isIntersecting | boolean | 是否进入视口 |
intersectionRatio | number | 当前可见比例(0~1) |
intersectionRect | DOMRectReadOnly | 元素与视口的交叉区域 |
rootBounds | DOMRectReadOnly | 根元素(root)的边界 |
time | number | 触发时间(毫秒) |
options包含以下信息:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
root | Element | null(视口) | 指定观察的父容器(默认为浏览器视口) |
rootMargin | string | "0px" | 扩展或缩小观察范围(类似 CSSmargin) |
threshold | number[] | [0] | 触发回调的可见比例阈值(如[0, 0.5, 1]) |
示例
2.观察目标元素
const options = {
root: document.querySelector("#scrollArea"), // 观察某个滚动容器
rootMargin: "10px", // 提前 10px 触发回调
threshold: [0, 0.5, 1] // 元素 0%、50%、100% 可见时触发
};
const target = document.querySelector("#target");
observer.observe(target); // 开始观察
3.回调函数
回调函数接收两个参数:
entries:一个数组,包含所有被观察元素的信息。observer:当前的IntersectionObserver实例。
const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log("元素进入视口", entry.target);
} else {
console.log("元素离开视口", entry.target);
}
});
};
- 停止观察
observer.unobserve(target); // 停止观察单个元素
observer.disconnect(); // 停止所有观察
常见应用场景
图片懒加载(Lazy Load)
<img data-src="image.jpg" class="lazy" />
const lazyImages = document.querySelectorAll(".lazy");
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 加载图片
observer.unobserve(img); // 停止观察已加载的图片
}
});
});
lazyImages.forEach(img => observer.observe(img));
无限滚动(Infinite Scroll)
const sentinel = document.querySelector("#loadMoreTrigger");
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMoreData(); // 加载更多内容
}
});
observer.observe(sentinel);
广告曝光统计
const ad = document.querySelector("#ad-banner");
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
logAdImpression(); // 记录广告曝光
observer.unobserve(ad);
}
}, { threshold: 0.5 }); // 至少 50% 可见才触发
observer.observe(ad);
动画触发(Scroll Animation)
const animatedElements = document.querySelectorAll(".animate-on-scroll");
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("fade-in");
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 }); // 10% 可见时触发动画
animatedElements.forEach(el => observer.observe(el));
实现懒加载和曝光统计:
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.onload = () => img.classList.add('loaded');
io.unobserve(img);
// 曝光统计
logImpression(img.dataset.id);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px 100px 0px' // 提前100px加载
});
document.querySelectorAll('img.lazy').forEach(img => {
io.observe(img);
});
与传统方法的对比
| 方法 | 性能 | 代码复杂度 | 适用场景 |
|---|---|---|---|
IntersectionObserver | ✅ 高性能(浏览器优化) | ✅ 简单 | 现代浏览器 |
scroll 事件 + getBoundingClientRect() | ❌ 频繁计算,性能差 | ❌ 复杂 | 兼容旧浏览器 |
3. ResizeObserver
ResizeObserver 是一个浏览器 API,用于高效监测 DOM 元素的尺寸变化(如 width、height),替代传统的 window.resize 事件或 MutationObserver 监听方式
** 基本用法**
** 创建 ResizeObserver**
const observer = new ResizeObserver(callback);
callback:当目标元素尺寸变化时触发的回调函数。
观察目标元素
const target = document.querySelector("#target");
observer.observe(target); // 开始观察
回调函数
回调函数接收两个参数:
entries:一个数组,包含所有被观察元素的尺寸信息。observer:当前的ResizeObserver实例。
const callback = (entries) => {
entries.forEach(entry => {
console.log("元素尺寸变化:", entry.contentRect);
});
};
每个 entry 包含以下关键信息:
| 属性 | 类型 | 说明 |
|---|---|---|
target | Element | 被观察的目标元素 |
contentRect | DOMRectReadOnly | 元素的尺寸和位置信息 |
borderBoxSize | ResizeObserverSize | 元素的边框盒尺寸(含border、padding) |
contentBoxSize | ResizeObserverSize | 元素的内容盒尺寸(不含border、padding) |
contentRect 结构
{
width: 200, // 元素宽度(px)
height: 100, // 元素高度(px)
x: 10, // 相对于视口的 X 坐标
y: 20, // 相对于视口的 Y 坐标
top: 20, // 等同于 y
right: 210, // x + width
bottom: 120, // y + height
left: 10 // 等同于 x
}
borderBoxSize 和 contentBoxSize
// 可能返回数组(某些浏览器支持多片段布局,如多列)
const { blockSize, inlineSize } = entry.contentBoxSize[0];
console.log("内容宽度:", inlineSize, "内容高度:", blockSize);
停止观察
observer.unobserve(target); // 停止观察单个元素
observer.disconnect(); // 停止所有观察
常见应用场景
响应式布局调整
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
console.log(`元素尺寸变化: ${width}x${height}`);
if (width < 600) {
entry.target.classList.add('mobile-layout');
} else {
entry.target.classList.remove('mobile-layout');
}
}
});
ro.observe(document.getElementById('responsive-container'));
图表/Canvas 自适应
const chartCanvas = document.querySelector("#chart");
const ctx = chartCanvas.getContext("2d");
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
chartCanvas.width = width;
chartCanvas.height = height;
redrawChart(); // 重新绘制图表
});
observer.observe(chartCanvas);
动态调整 iframe 高度
const iframe = document.querySelector("iframe");
const observer = new ResizeObserver((entries) => {
const { height } = entries[0].contentRect;
iframe.style.height = `${height}px`;
});
observer.observe(iframe.contentDocument.body);
检测元素隐藏/显示
const element = document.querySelector("#dynamicElement");
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
if (width === 0 || height === 0) {
console.log("元素被隐藏");
} else {
console.log("元素显示");
}
});
observer.observe(element);
与传统方法的对比
| 方法 | 性能 | 适用场景 | 缺点 |
|---|---|---|---|
ResizeObserver | ✅ 高效(浏览器优化) | 监测任意元素尺寸变化 | 不兼容 IE |
window.resize + getBoundingClientRect() | ❌ 频繁计算,性能差 | 仅窗口尺寸变化 | 不适用于单个元素 |
MutationObserver | ❌ 不适合尺寸监测 | DOM 结构变化 | 无法精确监测尺寸 |
总结与提升路径
核心要点回顾
- DOM 操作:
- 理解DOM树结构和节点类型
- 掌握查询、创建、修改和删除元素的API
- 使用文档片段和批量操作优化性能
- 事件处理:
- 理解捕获和冒泡机制
- 熟练使用事件委托模式
- 掌握常用事件类型和事件对象属性
- 现代实践:
- 使用MutationObserver监控DOM变化
- 实现懒加载和曝光统计
- 应用防抖和节流优化性能
进阶学习建议
- 深入浏览器工作原理:
- 了解重绘(repaint)和重排(reflow)机制
- 学习浏览器事件循环(event loop)
- 探索现代框架:
- 学习React/Vue的虚拟DOM实现
- 理解现代框架的事件系统
实战练习建议
- 实现一个完整的可排序、可拖拽的看板应用
- 开发一个带有实时验证和异步检查的表单系统
- 创建一个无限滚动的图片画廊,使用懒加载技术
- 实现一个自定义的右键上下文菜单系统
- 开发一个使用Web Workers处理大量DOM更新的应用
通过不断实践这些技术和模式,您将能够构建高性能、交互丰富的现代Web应用程序。