文章目录
- 一.事件监听的三种方式(从旧到新,推荐优先级排序)
-
- [1.行内监听(HTML 内联属性,不推荐)](#1.行内监听(HTML 内联属性,不推荐))
- [2.DOM 属性监听(元素属性赋值,简单场景可用)](#2.DOM 属性监听(元素属性赋值,简单场景可用))
- 3.addEventListener(标准监听方式,强烈推荐)
- 二.事件监听的核心概念
- 三.事件监听的进阶技巧
- 四.事件监听的性能优化
- 五.常见问题与解决方案
-
- 1.匿名函数无法解绑
- [2.this 指向问题](#2.this 指向问题)
- [3.focus/blur 事件不冒泡](#3.focus/blur 事件不冒泡)
- 4.事件委托中目标元素判断错误
- 六.总结
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(事件类型, 处理函数, 捕获 / 配置项);
关键参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| 事件类型 | 字符串 | 事件名称(如 click、input,不带 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();
优点:
- 支持为同一元素的同一事件绑定多个处理函数;
- 可控制事件在捕获 / 冒泡阶段触发;
- 提供丰富的配置项(如一次性事件、被动监听);
- 支持批量解绑(通过
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.高频事件的优化:防抖与节流
对于 resize、scroll、input、mousemove 等高频触发的事件,直接监听会导致函数频繁执行,引发性能问题,需通过防抖(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);
四.事件监听的性能优化
- 减少不必要的监听:仅为需要交互的元素绑定监听,避免无意义的监听;
- 及时解绑监听:
- 元素销毁时(如组件卸载),通过
removeEventListener解绑监听,避免内存泄漏; - 一次性事件使用
once: true配置,自动解绑;
- 元素销毁时(如组件卸载),通过
- 使用事件委托:替代逐个元素绑定,减少监听数量;
- 优化高频事件:防抖 / 节流降低函数执行频率;
- 移动端优化:
- 触摸事件(
touchstart/touchmove)添加passive: true,提升滚动流畅度; - 避免使用
click(有 300ms 延迟),可用touchend替代;
- 触摸事件(
- 批量解绑:通过
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 是最推荐的方式,其灵活性和功能性远胜其他方式.掌握事件委托、防抖节流、自定义事件等技巧,能有效提升代码的性能和可维护性.同时,注意及时解绑监听、优化高频事件,可避免内存泄漏和性能问题.