15.1 DOM 操作与事件委托

高效 DOM 操作

DOM 查询最佳实践

  1. 使用高效的查询方法
// 不推荐 - 返回动态集合(性能较差)
const divs = document.getElementsByTagName('div');

// 推荐 - 返回静态NodeList
const divs = document.querySelectorAll('div');
  1. 缩小查询范围
// 在特定容器内查询
const container = document.getElementById('app');
const buttons = container.querySelectorAll('.btn');
  1. 缓存DOM引用
// 避免重复查询
const cachedElements = {
  header: document.querySelector('header'),
  nav: document.getElementById('main-nav'),
  content: document.querySelector('.content')
};

DOM 操作优化

  1. 批量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);
  1. 使用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);
}
  1. 避免强制同步布局
// 不推荐 - 强制同步布局
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);
  }
});

高级事件委托实现

  1. 复杂选择器匹配
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`);
  }
});
  1. 性能优化版本
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);
  })
);
  1. 支持多个事件类型
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'));

性能与可维护性建议

  1. DOM操作优化

    • 最小化DOM访问次数
    • 使用requestAnimationFrame进行视觉变化
    • 避免在循环中进行DOM操作
  2. 事件处理优化

    • 合理使用事件委托
    • 及时移除不需要的事件监听器
    • 使用passive事件监听器提高滚动性能
    window.addEventListener('scroll', () => {}, { passive: true });
    
  3. 代码组织

    • 封装DOM操作逻辑为独立模块
    • 使用自定义事件解耦组件
    // 触发自定义事件
    const event = new CustomEvent('itemAdded', { detail: { item } });
    element.dispatchEvent(event);
    
    // 监听自定义事件
    element.addEventListener('itemAdded', (e) => {
      console.log('Item added:', e.detail.item);
    });
    
  4. 无障碍访问

    • 使用语义化HTML
    • 管理焦点顺序
    • 添加ARIA属性
    <button aria-label="Close" aria-expanded="false">X</button>
    
#前端开发 分享于 2025-03-25

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】