【WEB】DOM (五)进阶实践—— 事件处理与性能优化_通过dom事件禁止下载
文章目录
- 一、DOM 事件处理
-
- 1.1 事件流(事件传播)
- 1.2 事件绑定方式
-
- 1.2.1 HTML 属性绑定(内联事件)
- 1.2.2 DOM 属性绑定
- 1.2.3 addEventListener ()(推荐)
- 1.3 事件对象(Event)
- 1.4 常见事件类型
-
- 1.4.1 鼠标事件
- 1.4.2 键盘事件
- 1.4.3 表单事件
- 1.4.4 文档 / 窗口事件
- 1.5 事件委托(事件代理)
- 二、DOM 性能优化
-
- 2.1 减少 DOM 操作次数
- 2.2 避免频繁查询 DOM
- 2.3 使用高效的选择器
- 2.4 事件委托减少事件绑定
- 2.5 避免强制同步布局
- 2.6 使用虚拟滚动处理大量数据
一、DOM 事件处理
事件是用户与网页交互的基础(如点击、滚动、输入等),DOM 提供了完善的事件处理机制。
1.1 事件流(事件传播)
DOM 事件流描述了事件在 DOM 树中传播的过程,分为三个阶段:
- 捕获阶段:事件从 window 对象向下传播到目标元素的父节点
- 目标阶段:事件到达目标元素
- 冒泡阶段:事件从目标元素的父节点向上传播到 window 对象
1.2 事件绑定方式
1.2.1 HTML 属性绑定(内联事件)
直接在 HTML 标签中使用事件属性(如onclick
、onload
)绑定事件处理函数。
<button onclick=\"alert(\'按钮被点击了\')\">点击我</button><button onclick=\"handleClick()\">点击我</button><script> function handleClick() { alert(\'处理函数被调用\'); }</script>
缺点:
- HTML 与 JavaScript 代码混杂,不利于维护
- 同一个事件只能绑定一个处理函数
- 存在安全风险(如 XSS 攻击)
不推荐使用,仅用于简单示例或兼容旧代码。
1.2.2 DOM 属性绑定
通过元素的事件属性(如onclick
)绑定事件处理函数。
<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
(默认)表示在冒泡阶段处理
<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),包含事件的详细信息。
<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); // // 当前处理事件的元素(可能是目标元素的祖先) 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
:右键点击(上下文菜单)
<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
:按下并释放按键(主要用于字符键)
<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
:元素失去焦点
<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 加载完成(无需等待样式表、图片等)
// 页面完全加载完成(包括图片、样式表等)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 事件委托(事件代理)
事件委托是利用事件冒泡机制,将子元素的事件处理委托给父元素,从而实现高效的事件处理,特别适用于动态生成的元素。
优点:
- 减少事件绑定数量,提高性能
- 自动支持动态添加的子元素
- 简化代码,便于维护
<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 操作:
// 低效:多次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:
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 类批量修改样式:
// 低效:多次样式修改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。
// 低效:每次循环都查询DOMfor (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 选择器(如多层嵌套、伪类)
- 缩小查询范围(通过父元素调用选择方法):
// 高效:先找到父元素,再在父元素范围内查询const container = document.getElementById(\'container\');const items = container.getElementsByClassName(\'item\');
2.4 事件委托减少事件绑定
如前文所述,使用事件委托可以减少事件绑定的数量,尤其是对于大量相似元素(如列表项)。
// 低效:为每个列表项绑定事件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 操作会强制浏览器立即执行布局计算,这称为强制同步布局。
// 强制同步布局:先读取布局属性,再修改布局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 节点数量。
<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
)。