JavaScript进阶(四):DOM监听

文章目录

DOM 事件监听全面详解
事件监听( Event Listener)是 DOM 交互的核心机制,指为元素注册事件处理函数,当指定事件触发时自动执行该函数.它实现了「行为与结构分离」,是前端处理用户交互、页面状态变化的基础.以下从监听方式、核心配置、进阶技巧、性能优化、常见问题等维度系统拆解.

一.事件监听的三种方式(从旧到新,推荐优先级排序)

1.行内监听(HTML 内联属性,不推荐)

直接在 HTML 标签中通过 on+事件名 属性绑定处理函数,是最原始的方式.

语法:

html 复制代码
<element on[事件名]="处理函数(参数)"></element>

示例:

html 复制代码
<button onclick="handleClick('按钮1')">点击我</button>

<script>
  function handleClick(name) {
    alert(`你点击了${name}`);
  }
</script>

缺点:

  • 耦合度高: HTML 结构与 JS 逻辑混杂,不利于维护;
  • 安全风险: 若拼接用户输入,易引发 XSS 攻击;
  • 功能有限: 无法绑定多个同类型事件,也无法控制事件流阶段.

2.DOM 属性监听(元素属性赋值,简单场景可用)

通过给 DOM 元素的 on+事件名 属性赋值函数,实现事件绑定.

语法:

javascript 复制代码
元素.on[事件名] = 处理函数;

示例:

javascript 复制代码
const btn = document.querySelector('button');
// 绑定监听
btn.onclick = function (e) {
  console.log('点击事件触发', e.target);
};
// 解绑监听(赋值为 null 即可)
btn.onclick = null;

特点:

  • 优点: 语法简单,兼容性好(支持所有浏览器);
  • 缺点: 只能绑定一个处理函数,重新赋值会覆盖原有函数.

3.addEventListener(标准监听方式,强烈推荐)

W3C 制定的标准事件监听 API,是现代前端最常用的方式,支持多函数绑定、事件流控制、精细配置.

核心语法:

javascript 复制代码
// 绑定监听
元素.addEventListener(事件类型, 处理函数, 捕获 / 配置项);
// 解绑监听
元素.removeEventListener(事件类型, 处理函数, 捕获 / 配置项);

关键参数说明:

参数 类型 说明
事件类型 字符串 事件名称(如 clickinput,不带 on 前缀)
处理函数 函数 事件触发时执行的回调(接收事件对象 event 作为参数)
捕获 / 配置项 布尔值 / 对象 可选,默认 false(冒泡阶段触发);传对象可配置更多选项

基础示例:

javascript 复制代码
const btn = document.querySelector('button');
// 定义处理函数(需命名,方便解绑)
const clickHandler = function (e) {
  console.log('点击事件触发', e);
};

// 绑定监听(冒泡阶段)
btn.addEventListener('click', clickHandler);
// 绑定多个同类型事件(依次执行)
btn.addEventListener('click', () => {
  console.log('第二个点击处理函数');
});

// 解绑监听(必须传同一个函数引用,匿名函数无法解绑)
btn.removeEventListener('click', clickHandler);

高级配置项(第三个参数传对象):

javascript 复制代码
btn.addEventListener('scroll', handleScroll, {
  capture: false, // 是否在**捕获阶段**触发(默认 false,冒泡阶段)
  once: true, // 事件仅触发一次,触发后自动解绑
  passive: true, // 禁止处理函数中调用 e.preventDefault()(优化移动端滚动性能)
  signal: AbortSignal // 通过 AbortController 批量解绑事件(ES2021+)
});

signal 配置示例(批量解绑):

javascript 复制代码
const controller = new AbortController();
const { signal } = controller;

// 绑定多个事件,共用同一个 signal
btn.addEventListener('click', () => console.log('点击 1'), { signal });
btn.addEventListener('click', () => console.log('点击 2'), { signal });

// 批量解绑所有绑定的事件
controller.abort();

优点:

  1. 支持为同一元素的同一事件绑定多个处理函数;
  2. 可控制事件在捕获 / 冒泡阶段触发;
  3. 提供丰富的配置项(如一次性事件、被动监听);
  4. 支持批量解绑(通过 AbortController).

二.事件监听的核心概念

1.事件流与监听阶段

事件在 DOM 树中传播分为捕获阶段、目标阶段、冒泡阶段,addEventListener 的第三个参数决定监听函数在哪个阶段触发:

捕获阶段(capture: true):事件从 window 向下传播到目标元素时触发;

冒泡阶段(capture: false,默认):事件从目标元素向上传播到 window 时触发.

示例:

html 复制代码
<div class="parent">
  <button class="child">点击</button>
</div>

<script>
  const parent = document.querySelector('.parent');
  const child = document.querySelector('.child');

  // 捕获阶段监听
  parent.addEventListener('click', () => console.log('父元素-捕获'), true);
  // 冒泡阶段监听
  parent.addEventListener('click', () => console.log('父元素-冒泡'));
  child.addEventListener('click', () => console.log('子元素'));

  // 点击按钮,执行顺序:父元素-捕获 → 子元素 → 父元素-冒泡
</script>

2.事件对象(event)的核心作用

监听函数的第一个参数是事件对象,包含事件的所有关键信息,常用属性 / 方法:

成员 作用
e.target 事件实际触发的元素(事件源)
e.currentTarget 绑定事件的元素(等价于 this)
e.preventDefault() 阻止事件的默认行为(如链接跳转、表单提交)
e.stopPropagation() 阻止事件继续传播(捕获 / 冒泡)
e.stopImmediatePropagation() 阻止事件传播,且同一元素的后续监听函数不再执行

三.事件监听的进阶技巧

1.事件委托(代理)

利用事件冒泡,将子元素的事件监听绑定到父元素,实现「一次绑定,多元素生效」,尤其适用于动态生成的元素.

核心原理:

父元素监听事件后,通过 e.target 判断触发事件的子元素,执行对应逻辑.

示例(列表项点击监听):

html 复制代码
<ul id="list">
  <li>项1</li>
  <li>项2</li>
</ul>

<script>
const list = document.getElementById('list');
// 委托父元素绑定点击监听
list.addEventListener('click', (e) => {
  // 过滤目标元素(仅处理 li 标签)
  if (e.target.tagName === 'LI') {
    console.log('点击了列表项:', e.target.textContent);
  }
});

// 动态添加 li,无需重新绑定监听
const newLi = document.createElement('li');
newLi.textContent = '项3';
list.appendChild(newLi);

优点:

  • 减少事件监听的数量,降低内存占用;
  • 支持动态生成的元素,无需重复绑定;
  • 简化代码维护.

2.高频事件的优化:防抖与节流

对于 resizescrollinputmousemove 等高频触发的事件,直接监听会导致函数频繁执行,引发性能问题,需通过防抖(Debounce) 和节流(Throttle) 优化.

I.防抖(Debounce)

原理:

事件停止触发后,延迟一定时间再执行函数,若期间再次触发则重置延迟.

适用场景:

输入框实时搜索、窗口大小调整.

javascript 复制代码
// 防抖函数
function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用:输入框监听
const input = document.querySelector('input');
input.addEventListener(
  'input',
  debounce(function (e) {
    console.log('搜索关键词:', e.target.value);
  }, 300)
);

II.节流(Throttle)

原理:

限制函数的执行频率,每隔指定时间仅执行一次.

适用场景:

滚动加载、鼠标拖拽.

javascript 复制代码
// 节流函数
function throttle(fn, interval = 500) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 使用:页面滚动监听
window.addEventListener(
  'scroll',
  throttle(function () {
    console.log('滚动中...');
  }, 500)
);

3.自定义事件监听

除了浏览器内置事件,还可以创建自定义事件,通过 dispatchEvent 手动触发,适用于组件通信、自定义交互.

示例:

javascript 复制代码
const btn = document.querySelector('button');

// 1. 创建自定义事件(可携带自定义数据)
const myEvent = new CustomEvent('custom-click', {
  detail: { id: 123 }, // 自定义数据
  bubbles: true // 允许冒泡
});

// 2. 绑定自定义事件监听
btn.addEventListener('custom-click', (e) => {
  console.log('自定义事件触发:', e.detail.id);
});

// 3. 手动触发自定义事件
btn.dispatchEvent(myEvent);

四.事件监听的性能优化

  1. 减少不必要的监听:仅为需要交互的元素绑定监听,避免无意义的监听;
  2. 及时解绑监听:
    • 元素销毁时(如组件卸载),通过 removeEventListener 解绑监听,避免内存泄漏;
    • 一次性事件使用 once: true 配置,自动解绑;
  3. 使用事件委托:替代逐个元素绑定,减少监听数量;
  4. 优化高频事件:防抖 / 节流降低函数执行频率;
  5. 移动端优化:
    • 触摸事件(touchstart/touchmove)添加 passive: true,提升滚动流畅度;
    • 避免使用 click(有 300ms 延迟),可用 touchend 替代;
  6. 批量解绑:通过 AbortController 批量管理多个监听,简化解绑逻辑.

五.常见问题与解决方案

1.匿名函数无法解绑

问题:

使用匿名函数绑定的监听,无法通过 removeEventListener 解绑.

解决:

使用命名函数或保存函数引用.

javascript 复制代码
// 错误:匿名函数无法解绑
btn.addEventListener('click', () => console.log('点击'));
btn.removeEventListener('click', () => console.log('点击')); // 无效

// 正确:命名函数
const handler = () => console.log('点击');
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler); // 有效

2.this 指向问题

问题:

箭头函数作为监听函数时,this 不指向绑定元素(指向外层作用域).

解决:

普通函数的 this 指向绑定元素,或用 e.currentTarget 获取.

javascript 复制代码
btn.addEventListener('click', function () {
  console.log(this); // 指向 btn 元素
});

btn.addEventListener('click', (e) => {
  console.log(e.currentTarget); // 指向 btn 元素(替代 this)
});

3.focus/blur 事件不冒泡

问题:
focus/blur 事件不支持冒泡,无法使用事件委托.

解决:

使用 focusin/focusout 事件(支持冒泡)替代.

javascript 复制代码
// 替代 focus
parent.addEventListener('focusin', (e) => {
  if (e.target.tagName === 'INPUT') {
    console.log('输入框获取焦点');
  }
});

4.事件委托中目标元素判断错误

问题:

子元素包含嵌套标签时,e.target 可能指向子标签而非目标元素.

解决:

通过向上遍历找到目标元素.

javascript 复制代码
list.addEventListener('click', (e) => {
  let target = e.target;
  // 向上遍历,找到 li 元素
  while (target && target.tagName !== 'LI') {
    target = target.parentNode;
  }
  if (target) {
    console.log('点击了 li:', target.textContent);
  }
});

六.总结

事件监听是前端交互的核心,addEventListener 是最推荐的方式,其灵活性和功能性远胜其他方式.掌握事件委托、防抖节流、自定义事件等技巧,能有效提升代码的性能和可维护性.同时,注意及时解绑监听、优化高频事件,可避免内存泄漏和性能问题.

相关推荐
清晓粼溪2 小时前
统一异常处理
java·开发语言
syt_10132 小时前
grid布局之-子项放置4
开发语言·javascript·ecmascript
喵了meme2 小时前
C语言实战2
c语言·开发语言·网络
charlie1145141912 小时前
现代C++工程实践:简单的IniParser3——改进我们的split
开发语言·c++·笔记·学习
fish_xk2 小时前
c++的引用和类的初见
开发语言·c++
yong99903 小时前
水箱水位控制系统MATLAB实现
开发语言·matlab
通往曙光的路上3 小时前
授权vvvvvv
java·开发语言·windows
Data_agent3 小时前
京东商品视频API,Python请求示例
java·开发语言·爬虫·python
a努力。3 小时前
HSBC Java面试被问:CAS如何解决ABA问题
java·开发语言·面试