文章目录
- [一、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 标签中使用事件属性(如onclick
、onload
)绑定事件处理函数。
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
:表单元素的值改变(通常用于select
、checkbox
等)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 的几何结构发生变化(如尺寸、位置改变)时,浏览器需要重新计算元素的位置和大小,并重新构建渲染树,这个过程称为重排。
- 重绘:当元素的外观发生变化(如颜色、背景改变)但不影响布局时,浏览器只需重新绘制元素,这个过程称为重绘。
重排必然导致重绘,重绘不一定导致重排。
- 优化策略:
- 合并 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); // 只触发一次重排
- 离线操作 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);
- 使用 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
(最快,基于哈希表)getElementsByTagName
、getElementsByClassName
querySelector
、querySelectorAll
(功能强大但性能略低)
优化建议:
- 优先使用
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-virtualized
、vue-virtual-scroller
)。