15.1 DOM 操作与事件委托
高效 DOM 操作
DOM 查询最佳实践
- 使用高效的查询方法:
// 不推荐 - 返回动态集合(性能较差)
const divs = document.getElementsByTagName('div');
// 推荐 - 返回静态NodeList
const divs = document.querySelectorAll('div');
- 缩小查询范围:
// 在特定容器内查询
const container = document.getElementById('app');
const buttons = container.querySelectorAll('.btn');
- 缓存DOM引用:
// 避免重复查询
const cachedElements = {
header: document.querySelector('header'),
nav: document.getElementById('main-nav'),
content: document.querySelector('.content')
};
DOM 操作优化
- 批量DOM修改:
// 不推荐 - 多次重排
for (let i = 0; i < 100; i++) {
document.body.appendChild(document.createElement('div'));
}
// 推荐 - 使用文档片段
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
fragment.appendChild(document.createElement('div'));
}
document.body.appendChild(fragment);
- 使用
requestAnimationFrame:
function animateElement(element) {
let start = null;
function step(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
element.style.transform = `translateX(${Math.min(progress / 10, 200)}px`;
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
}
- 避免强制同步布局:
// 不推荐 - 强制同步布局
function resizeAllParagraphs() {
const paragraphs = document.querySelectorAll('p');
for (let p of paragraphs) {
p.style.width = `${p.offsetWidth + 10}px`; // 读取offsetWidth导致强制布局
}
}
// 推荐 - 先读取后写入
function resizeAllParagraphsOptimized() {
const paragraphs = document.querySelectorAll('p');
const widths = Array.from(paragraphs).map(p => p.offsetWidth);
paragraphs.forEach((p, i) => {
p.style.width = `${widths[i] + 10}px`;
});
}
事件委托模式
基本原理
事件委托利用事件冒泡机制,在父元素上处理子元素的事件:
document.getElementById('parent').addEventListener('click', function(event) {
if (event.target.matches('.child')) {
console.log('Child element clicked:', event.target);
}
});
高级事件委托实现
- 复杂选择器匹配:
document.body.addEventListener('click', function(event) {
const button = event.target.closest('[data-action]');
if (button) {
const action = button.dataset.action;
console.log(`Action "${action}" triggered`);
}
});
- 性能优化版本:
function createDelegatedHandler(selector, handler) {
return function(event) {
let target = event.target;
while (target && target !== this) {
if (target.matches(selector)) {
handler.call(target, event);
return;
}
target = target.parentNode;
}
};
}
// 使用示例
document.body.addEventListener(
'click',
createDelegatedHandler('.btn', function(event) {
console.log('Button clicked:', this);
})
);
- 支持多个事件类型:
const eventDelegator = {
events: {},
addDelegatedListener(parent, eventType, selector, handler) {
const key = `${eventType}|${selector}`;
if (!this.events[key]) {
this.events[key] = [];
parent.addEventListener(eventType, (event) => {
const target = event.target.closest(selector);
if (target) {
this.events[key].forEach(h => h.call(target, event));
}
});
}
this.events[key].push(handler);
}
};
// 使用示例
eventDelegator.addDelegatedListener(
document.body,
'click',
'.item',
function() { console.log('Item clicked:', this); }
);
现代DOM操作API
MutationObserver 监控DOM变化
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
console.log('Nodes changed:', mutation.addedNodes, mutation.removedNodes);
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true
});
// 停止观察
// observer.disconnect();
IntersectionObserver 实现懒加载
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
lazyImages.forEach(img => imageObserver.observe(img));
ResizeObserver 监控元素尺寸变化
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
console.log(`Element size changed: ${width}x${height}`);
}
});
resizeObserver.observe(document.getElementById('resizable-element'));
实战案例
案例1:可排序列表
class SortableList {
constructor(element) {
this.list = element;
this.items = [...element.children];
this.setupEvents();
}
setupEvents() {
this.list.addEventListener('mousedown', this.handleDragStart.bind(this));
document.addEventListener('mousemove', this.handleDrag.bind(this));
document.addEventListener('mouseup', this.handleDragEnd.bind(this));
}
handleDragStart(e) {
if (!e.target.matches('li')) return;
this.draggedItem = e.target;
this.placeholder = document.createElement('li');
this.placeholder.className = 'placeholder';
// 计算初始位置
this.startY = e.clientY;
this.startIndex = [...this.list.children].indexOf(this.draggedItem);
// 设置占位符
this.draggedItem.style.opacity = '0.5';
this.draggedItem.after(this.placeholder);
// 克隆拖动元素
this.clone = this.draggedItem.cloneNode(true);
this.clone.style.position = 'absolute';
this.clone.style.pointerEvents = 'none';
this.clone.style.width = `${this.draggedItem.offsetWidth}px`;
this.clone.style.height = `${this.draggedItem.offsetHeight}px`;
this.clone.style.top = `${this.draggedItem.getBoundingClientRect().top}px`;
this.clone.style.left = `${this.draggedItem.getBoundingClientRect().left}px`;
document.body.appendChild(this.clone);
}
handleDrag(e) {
if (!this.draggedItem) return;
// 更新克隆元素位置
this.clone.style.top = `${e.clientY}px`;
// 计算新位置
const items = [...this.list.children].filter(item => item !== this.placeholder);
const currentY = e.clientY;
const currentIndex = items.findIndex(item => {
const rect = item.getBoundingClientRect();
return currentY < rect.top + rect.height / 2;
});
if (currentIndex >= 0 && currentIndex !== this.currentIndex) {
this.currentIndex = currentIndex;
const targetItem = items[currentIndex];
targetItem.before(this.placeholder);
}
}
handleDragEnd() {
if (!this.draggedItem) return;
// 移动元素到新位置
this.placeholder.replaceWith(this.draggedItem);
this.draggedItem.style.opacity = '';
this.clone.remove();
// 清理
this.draggedItem = null;
this.clone = null;
this.placeholder = null;
}
}
// 使用
new SortableList(document.getElementById('sortable-list'));
案例2:动态表单验证
class FormValidator {
constructor(formElement) {
this.form = formElement;
this.fields = Array.from(formElement.querySelectorAll('[data-validate]'));
this.errors = new Map();
// 事件委托处理输入验证
this.form.addEventListener('input', this.handleInput.bind(this));
this.form.addEventListener('submit', this.handleSubmit.bind(this));
// 初始化验证
this.fields.forEach(field => this.validateField(field));
}
handleInput(event) {
const field = event.target;
if (field.hasAttribute('data-validate')) {
this.validateField(field);
}
}
validateField(field) {
const rules = field.dataset.validate.split('|');
let isValid = true;
for (const rule of rules) {
const [ruleName, ruleValue] = rule.split(':');
switch (ruleName) {
case 'required':
if (!field.value.trim()) {
this.setError(field, 'This field is required');
isValid = false;
}
break;
case 'min':
if (field.value.length < parseInt(ruleValue)) {
this.setError(field, `Minimum ${ruleValue} characters`);
isValid = false;
}
break;
case 'email':
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field.value)) {
this.setError(field, 'Invalid email format');
isValid = false;
}
break;
}
if (!isValid) break;
}
if (isValid) {
this.clearError(field);
}
return isValid;
}
setError(field, message) {
let errorElement = field.nextElementSibling;
if (!errorElement || !errorElement.classList.contains('error-message')) {
errorElement = document.createElement('div');
errorElement.className = 'error-message';
field.after(errorElement);
}
errorElement.textContent = message;
field.classList.add('invalid');
this.errors.set(field, message);
}
clearError(field) {
const errorElement = field.nextElementSibling;
if (errorElement && errorElement.classList.contains('error-message')) {
errorElement.remove();
}
field.classList.remove('invalid');
this.errors.delete(field);
}
handleSubmit(event) {
let isValid = true;
this.fields.forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
if (!isValid) {
event.preventDefault();
// 滚动到第一个错误字段
const firstErrorField = this.fields.find(f => this.errors.has(f));
if (firstErrorField) {
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstErrorField.focus();
}
}
}
}
// 使用
new FormValidator(document.getElementById('signup-form'));
性能与可维护性建议
-
DOM操作优化:
- 最小化DOM访问次数
- 使用
requestAnimationFrame进行视觉变化 - 避免在循环中进行DOM操作
-
事件处理优化:
- 合理使用事件委托
- 及时移除不需要的事件监听器
- 使用
passive事件监听器提高滚动性能
window.addEventListener('scroll', () => {}, { passive: true }); -
代码组织:
- 封装DOM操作逻辑为独立模块
- 使用自定义事件解耦组件
// 触发自定义事件 const event = new CustomEvent('itemAdded', { detail: { item } }); element.dispatchEvent(event); // 监听自定义事件 element.addEventListener('itemAdded', (e) => { console.log('Item added:', e.detail.item); }); -
无障碍访问:
- 使用语义化HTML
- 管理焦点顺序
- 添加ARIA属性
<button aria-label="Close" aria-expanded="false">X</button>
#前端开发
分享于 2025-03-25