WEB:DOM (五)进阶实践—— 事件处理与性能优化

文章目录

  • [一、DOM 事件处理](#一、DOM 事件处理)
    • [1.1 事件流(事件传播)](#1.1 事件流(事件传播))
    • [1.2 事件绑定方式](#1.2 事件绑定方式)
      • [1.2.1 HTML 属性绑定(内联事件)](#1.2.1 HTML 属性绑定(内联事件))
      • [1.2.2 DOM 属性绑定](#1.2.2 DOM 属性绑定)
      • [1.2.3 addEventListener ()(推荐)](#1.2.3 addEventListener ()(推荐))
    • [1.3 事件对象(Event)](#1.3 事件对象(Event))
    • [1.4 常见事件类型](#1.4 常见事件类型)
      • [1.4.1 鼠标事件](#1.4.1 鼠标事件)
      • [1.4.2 键盘事件](#1.4.2 键盘事件)
      • [1.4.3 表单事件](#1.4.3 表单事件)
      • [1.4.4 文档 / 窗口事件](#1.4.4 文档 / 窗口事件)
    • [1.5 事件委托(事件代理)](#1.5 事件委托(事件代理))
  • [二、DOM 性能优化](#二、DOM 性能优化)
    • [2.1 减少 DOM 操作次数](#2.1 减少 DOM 操作次数)
    • [2.2 避免频繁查询 DOM](#2.2 避免频繁查询 DOM)
    • [2.3 使用高效的选择器](#2.3 使用高效的选择器)
    • [2.4 事件委托减少事件绑定](#2.4 事件委托减少事件绑定)
    • [2.5 避免强制同步布局](#2.5 避免强制同步布局)
    • [2.6 使用虚拟滚动处理大量数据](#2.6 使用虚拟滚动处理大量数据)

一、DOM 事件处理

事件是用户与网页交互的基础(如点击、滚动、输入等),DOM 提供了完善的事件处理机制。

1.1 事件流(事件传播)

DOM 事件流描述了事件在 DOM 树中传播的过程,分为三个阶段:

  • 捕获阶段:事件从 window 对象向下传播到目标元素的父节点
  • 目标阶段:事件到达目标元素
  • 冒泡阶段:事件从目标元素的父节点向上传播到 window 对象

1.2 事件绑定方式

1.2.1 HTML 属性绑定(内联事件)

直接在 HTML 标签中使用事件属性(如onclickonload)绑定事件处理函数。

html 复制代码
<!-- 直接写JavaScript代码 -->
<button onclick="alert('按钮被点击了')">点击我</button>

<!-- 调用函数 -->
<button onclick="handleClick()">点击我</button>

<script>
  function handleClick() {
    alert('处理函数被调用');
  }
</script>

缺点:

  • HTML 与 JavaScript 代码混杂,不利于维护
  • 同一个事件只能绑定一个处理函数
  • 存在安全风险(如 XSS 攻击)

不推荐使用,仅用于简单示例或兼容旧代码。

1.2.2 DOM 属性绑定

通过元素的事件属性(如onclick)绑定事件处理函数。

html 复制代码
<button id="myBtn">点击我</button>

<script>
  const btn = document.getElementById('myBtn');
  
  // 绑定事件处理函数
  btn.onclick = function() {
    alert('按钮被点击了');
  };
  
  // 绑定命名函数
  function handleClick() {
    console.log('处理点击事件');
  }
  btn.onclick = handleClick;
  
  // 移除事件处理
  btn.onclick = null;
  
  // 同一个事件只能绑定一个处理函数(后面的会覆盖前面的)
  btn.onclick = function() {
    console.log('新的处理函数');
  }; // 前面的handleClick会被覆盖
</script>
  • 缺点:
    • 同一个事件只能绑定一个处理函数
    • 无法控制事件传播阶段(只能在冒泡阶段处理)
  • 优点:
    • 简单直观,兼容性好

1.2.3 addEventListener ()(推荐)

现代 DOM 标准推荐的事件绑定方法,功能强大灵活。

语法:

绑定: element.addEventListener(eventType, handler, useCapture)

移除: element.removeEventListener(evenType, handler,useCapture)

  • eventType:事件类型(如'click''mouseover',不含on前缀)
  • handler:事件处理函数
  • useCapture:布尔值,true表示在捕获阶段处理事件false(默认)表示在冒泡阶段处理
html 复制代码
<button id="myBtn">点击我</button>

<script>
  const btn = document.getElementById('myBtn');
  
  // 绑定事件处理函数
  btn.addEventListener('click', function() {
    console.log('点击事件处理1');
  });
  
  // 可以绑定多个处理函数
  btn.addEventListener('click', function() {
    console.log('点击事件处理2');
  });
  
  // 使用命名函数
  function handleClick() {
    console.log('命名函数处理点击');
  }
  btn.addEventListener('click', handleClick);
  
  // 在捕获阶段处理事件
  document.body.addEventListener('click', function() {
    console.log('body捕获阶段处理');
  }, true);
  
  // 移除事件处理函数(必须使用命名函数)
  btn.removeEventListener('click', handleClick);
</script>

优点:

  • 可以为同一个事件绑定多个处理函数
  • 可以控制在捕获阶段还是冒泡阶段处理事件
  • 支持更多类型的事件
  • 可以更灵活地移除事件处理函数

1.3 事件对象(Event)

事件处理函数被调用时,会自动接收一个事件对象(Event),包含事件的详细信息。

html 复制代码
<button id="myBtn">点击我</button>

<script>
  const btn = document.getElementById('myBtn');
  
  btn.addEventListener('click', function(event) {
    // 事件对象
    console.log(event);
    
    // 事件类型
    console.log(event.type); // "click"
    
    // 事件目标(触发事件的元素)
    console.log(event.target); // <button id="myBtn">点击我</button>
    
    // 当前处理事件的元素(可能是目标元素的祖先)
    console.log(event.currentTarget); // 同上,因为事件绑定在按钮上
    
    // 阻止事件默认行为
    event.preventDefault();
    
    // 阻止事件传播(冒泡或捕获)
    event.stopPropagation();
    
    // 鼠标点击位置
    console.log('X坐标:', event.clientX); // 相对于视口的X坐标
    console.log('Y坐标:', event.clientY); // 相对于视口的Y坐标
  });
</script>

常用的事件对象属性和方法:

  • type:事件类型
  • target:事件的目标元素
  • currentTarget:当前处理事件的元素(与this相同)
  • preventDefault():阻止事件的默认行为(如链接跳转、表单提交)
  • stopPropagation():阻止事件继续传播(冒泡或捕获)
  • stopImmediatePropagation():阻止事件传播,并且阻止当前元素上的其他事件处理函数执行
  • bubbles:事件是否冒泡
  • cancelable:事件是否可以取消默认行为

1.4 常见事件类型

DOM 定义了多种事件类型,以下是一些常用的:

1.4.1 鼠标事件

  • click:鼠标点击元素
  • dblclick:鼠标双击元素
  • mousedown:鼠标按下
  • mouseup:鼠标释放
  • mouseover:鼠标移动到元素上
  • mouseout:鼠标从元素上移开
  • mousemove:鼠标在元素上移动
  • contextmenu:右键点击(上下文菜单)
html 复制代码
<div id="mouseArea" style="width: 200px; height: 200px; background: lightgray;"></div>

<script>
  const area = document.getElementById('mouseArea');
  
  area.addEventListener('mouseover', () => {
    area.textContent = '鼠标进入';
  });
  
  area.addEventListener('mouseout', () => {
    area.textContent = '鼠标离开';
  });
  
  area.addEventListener('mousemove', (e) => {
    const x = e.offsetX; // 相对于元素的X坐标
    const y = e.offsetY; // 相对于元素的Y坐标
    area.textContent = `X: ${x}, Y: ${y}`;
  });
</script>

1.4.2 键盘事件

  • keydown:按下键盘按键
  • keyup:释放键盘按键
  • keypress:按下并释放按键(主要用于字符键)
html 复制代码
<input type="text" id="keyInput" placeholder="按下键盘">

<script>
  const input = document.getElementById('keyInput');
  
  input.addEventListener('keydown', (e) => {
    console.log(`按下了键: ${e.key}, 键码: ${e.keyCode}`);
    // 阻止默认行为(如阻止输入特定字符)
    if (e.key === ' ') {
      e.preventDefault();
      alert('不允许输入空格');
    }
  });
  
  input.addEventListener('keyup', (e) => {
    console.log(`释放了键: ${e.key}`);
  });
</script>

1.4.3 表单事件

  • submit:表单提交
  • reset:表单重置
  • change:表单元素的值改变(通常用于 selectcheckbox 等)
  • input:表单元素的值发生变化(实时)
  • focus:元素获得焦点
  • blur:元素失去焦点
html 复制代码
<form id="myForm">
  <input type="text" name="username" placeholder="用户名">
  <input type="password" name="password" placeholder="密码">
  <button type="submit">提交</button>
</form>

<script>
  const form = document.getElementById('myForm');
  
  // 表单提交事件
  form.addEventListener('submit', (e) => {
    e.preventDefault(); // 阻止表单默认提交行为
    
    // 获取表单数据
    const username = form.elements.username.value;
    const password = form.elements.password.value;
    
    console.log(`用户名: ${username}, 密码: ${password}`);
    // 这里可以添加AJAX提交逻辑
  });
  
  // 输入事件
  const usernameInput = form.elements.username;
  usernameInput.addEventListener('input', (e) => {
    console.log(`输入的内容: ${e.target.value}`);
  });
</script>

1.4.4 文档 / 窗口事件

  • load:页面或资源加载完成
  • unload:页面卸载(关闭或刷新)
  • resize:窗口大小改变
  • scroll:页面滚动
  • DOMContentLoaded:DOM 加载完成(无需等待样式表、图片等)
javascript 复制代码
// 页面完全加载完成(包括图片、样式表等)
window.addEventListener('load', () => {
  console.log('页面完全加载完成');
});

// DOM加载完成(更快)
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM加载完成');
  // 可以在这里开始操作DOM
});

// 窗口大小改变
window.addEventListener('resize', () => {
  console.log(`窗口大小: ${window.innerWidth}x${window.innerHeight}`);
});

// 页面滚动
window.addEventListener('scroll', () => {
  console.log(`滚动位置: ${window.scrollY}px`);
  // 显示/隐藏回到顶部按钮等逻辑
});

1.5 事件委托(事件代理)

事件委托是利用事件冒泡机制,将子元素的事件处理委托给父元素,从而实现高效的事件处理,特别适用于动态生成的元素。
优点:

  • 减少事件绑定数量,提高性能
  • 自动支持动态添加的子元素
  • 简化代码,便于维护
html 复制代码
<ul id="itemList">
  <li>项目1</li>
  <li>项目2</li>
  <li>项目3</li>
</ul>
<button id="addItem">添加项目</button>

<script>
  const list = document.getElementById('itemList');
  const addBtn = document.getElementById('addItem');
  
  // 事件委托:将li的事件委托给ul处理
  list.addEventListener('click', (e) => {
    // 判断事件目标是否是li元素
    if (e.target.tagName === 'LI') {
      console.log(`点击了项目: ${e.target.textContent}`);
      e.target.style.backgroundColor = 'lightblue';
    }
  });
  
  // 动态添加项目
  let count = 3;
  addBtn.addEventListener('click', () => {
    count++;
    const li = document.createElement('li');
    li.textContent = `项目${count}`;
    list.appendChild(li);
    // 新添加的li无需单独绑定事件,事件委托会处理
  });
</script>

事件委托的核心思想是:在父元素上监听事件,通过事件对象的target属性判断实际触发事件的子元素。

二、DOM 性能优化

DOM 操作是前端性能的主要瓶颈之一,不合理的 DOM 操作会导致页面卡顿、响应缓慢。以下是一些 DOM 性能优化的关键技巧。

2.1 减少 DOM 操作次数

DOM 操作(尤其是添加、删除、修改节点)会触发浏览器的重排(回流) 和重绘,这是非常耗费性能的操作。

  • 重排(回流):当 DOM 的几何结构发生变化(如尺寸、位置改变)时,浏览器需要重新计算元素的位置和大小,并重新构建渲染树,这个过程称为重排。
  • 重绘:当元素的外观发生变化(如颜色、背景改变)但不影响布局时,浏览器只需重新绘制元素,这个过程称为重绘。

重排必然导致重绘,重绘不一定导致重排。

  • 优化策略:
    1. 合并 DOM 操作:
javascript 复制代码
// 低效:多次DOM操作
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `项目 ${i}`;
  list.appendChild(li); // 每次都触发重排
}

// 高效:合并操作
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); // 只触发一次重排
  1. 离线操作 DOM:
javascript 复制代码
const container = document.getElementById('container');
// 克隆元素进行离线操作
const clone = container.cloneNode(true);

// 在克隆元素上进行大量操作
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `内容 ${i}`;
  clone.appendChild(div);
}

// 替换原始元素(一次重排)
container.parentNode.replaceChild(clone, container);
  1. 使用 CSS 类批量修改样式:
javascript 复制代码
// 低效:多次样式修改
const element = document.getElementById('box');
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';

// 高效:使用CSS类
.box-styles {
  width: 100px;
  height: 100px;
  background-color: red;
}

element.classList.add('box-styles');

2.2 避免频繁查询 DOM

DOM 查询(尤其是复杂的选择器)是比较耗时的操作,应避免在循环中频繁查询 DOM

javascript 复制代码
// 低效:每次循环都查询DOM
for (let i = 0; i < 1000; i++) {
  document.getElementById('list').appendChild(createElement(i));
}

// 高效:缓存DOM查询结果
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {
  list.appendChild(createElement(i));
}

2.3 使用高效的选择器

不同的 DOM 选择方法性能差异很大,选择合适的选择器可以提高查询效率。

性能从高到低排序:

  • getElementById(最快,基于哈希表)
  • getElementsByTagNamegetElementsByClassName
  • querySelectorquerySelectorAll(功能强大但性能略低)

优化建议:

  • 优先使用getElementById获取单个元素
  • 避免使用复杂的 CSS 选择器(如多层嵌套、伪类)
  • 缩小查询范围(通过父元素调用选择方法):
javascript 复制代码
// 高效:先找到父元素,再在父元素范围内查询
const container = document.getElementById('container');
const items = container.getElementsByClassName('item');

2.4 事件委托减少事件绑定

如前文所述,使用事件委托可以减少事件绑定的数量,尤其是对于大量相似元素(如列表项)。

javascript 复制代码
// 低效:为每个列表项绑定事件
const items = document.querySelectorAll('li.item');
items.forEach(item => {
  item.addEventListener('click', handleClick);
});

// 高效:使用事件委托
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
  if (e.target.classList.contains('item')) {
    handleClick.call(e.target, e);
  }
});

2.5 避免强制同步布局

浏览器为了优化性能,会将布局操作推迟到必要时才执行。但某些 DOM 操作会强制浏览器立即执行布局计算,这称为强制同步布局。

javascript 复制代码
// 强制同步布局:先读取布局属性,再修改布局
const elements = document.querySelectorAll('.box');

elements.forEach(element => {
  // 读取布局属性(触发布局计算)
  const width = element.offsetWidth;
  
  // 修改布局属性(本可以批量处理)
  element.style.width = `${width + 10}px`;
});

// 优化:先读取所有属性,再统一修改
const widths = [];
// 第一阶段:只读取属性
elements.forEach(element => {
  widths.push(element.offsetWidth);
});

// 第二阶段:统一修改属性
elements.forEach((element, index) => {
  element.style.width = `${widths[index] + 10}px`;
});

2.6 使用虚拟滚动处理大量数据

当需要展示大量数据(如万级以上列表)时,直接渲染所有 DOM 节点会导致严重的性能问题。虚拟滚动技术只渲染可视区域内的节点,大大减少 DOM 节点数量。

html 复制代码
<div id="virtual-list" style="height: 500px; overflow: auto; border: 1px solid #ccc;">
  <div id="list-container"></div>
</div>

<script>
  // 模拟大量数据(10万条)
  const totalCount = 100000;
  const itemHeight = 50; // 每个项的高度
  const visibleCount = 10; // 可视区域可显示的项数
  
  const list = document.getElementById('virtual-list');
  const container = document.getElementById('list-container');
  
  // 设置容器高度(让滚动条正常显示)
  container.style.height = `${totalCount * itemHeight}px`;
  
  // 渲染可视区域的项
  function renderVisibleItems() {
    const scrollTop = list.scrollTop;
    // 计算可视区域起始索引
    const startIndex = Math.floor(scrollTop / itemHeight);
    // 计算需要渲染的项(比可视区域多渲染几项,避免快速滚动时出现空白)
    const renderStart = Math.max(0, startIndex - 2);
    const renderEnd = Math.min(totalCount, startIndex + visibleCount + 2);
    
    // 清空容器
    container.innerHTML = '';
    
    // 设置偏移量(让渲染的项显示在正确位置)
    container.style.paddingTop = `${renderStart * itemHeight}px`;
    
    // 渲染可见项
    for (let i = renderStart; i < renderEnd; i++) {
      const item = document.createElement('div');
      item.style.height = `${itemHeight}px`;
      item.style.borderBottom = '1px solid #eee';
      item.textContent = `项目 ${i + 1}`;
      container.appendChild(item);
    }
  }
  
  // 初始渲染
  renderVisibleItems();
  
  // 滚动时重新渲染
  list.addEventListener('scroll', renderVisibleItems);
</script>

虚拟滚动是处理大数据列表的常用方案,实际项目中可以使用成熟的库(如react-virtualizedvue-virtual-scroller)。

相关推荐
2301_781668615 小时前
前端基础 JS Vue3 Ajax
前端
上单带刀不带妹5 小时前
前端安全问题怎么解决
前端·安全
Fly-ping5 小时前
【前端】JavaScript 的事件循环 (Event Loop)
开发语言·前端·javascript
SunTecTec6 小时前
IDEA 类上方注释 签名
服务器·前端·intellij-idea
在逃的吗喽6 小时前
黑马头条项目详解
前端·javascript·ajax
袁煦丞6 小时前
有Nextcloud家庭共享不求人:cpolar内网穿透实验室第471个成功挑战
前端·程序员·远程工作
小磊哥er7 小时前
【前端工程化】前端项目开发过程中如何做好通知管理?
前端
拾光拾趣录7 小时前
一次“秒开”变成“转菊花”的线上事故
前端
你我约定有三7 小时前
前端笔记:同源策略、跨域问题
前端·笔记
JHCan3337 小时前
一个没有手动加分号引发的bug
前端·javascript·bug