> 技术文档 > 【WEB】DOM (五)进阶实践—— 事件处理与性能优化_通过dom事件禁止下载

【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 标签中使用事件属性(如onclickonload)绑定事件处理函数。

<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>

【WEB】DOM (五)进阶实践—— 事件处理与性能优化_通过dom事件禁止下载

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

  • 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>

【WEB】DOM (五)进阶实践—— 事件处理与性能优化_通过dom事件禁止下载

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>

【WEB】DOM (五)进阶实践—— 事件处理与性能优化_通过dom事件禁止下载

1.4.3 表单事件

  • submit:表单提交
  • reset:表单重置
  • change:表单元素的值改变(通常用于 selectcheckbox 等)
  • 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>

【WEB】DOM (五)进阶实践—— 事件处理与性能优化_通过dom事件禁止下载

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>

【WEB】DOM (五)进阶实践—— 事件处理与性能优化_通过dom事件禁止下载

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

二、DOM 性能优化

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

2.1 减少 DOM 操作次数

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

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

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

  • 优化策略:
    1. 合并 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); // 只触发一次重排
  1. 离线操作 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);
  1. 使用 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(最快,基于哈希表)
  • getElementsByTagNamegetElementsByClassName
  • querySelectorquerySelectorAll(功能强大但性能略低)

优化建议:

  • 优先使用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-virtualizedvue-virtual-scroller)。