一、事件是怎么“发生”的?—— 三步走的“事件接力赛”
1.1 DOM树的“快递投递”逻辑
想象你点击了页面上的一个按钮,这个动作会像快递包裹一样经历三段旅程:
<div id="parent">
<div id="child"></div>
</div>
事件流三阶段:
- 捕获阶段(快递从省→市→区)
事件从document出发,经过<html>→<body>→#parent→#child - 目标阶段(快递员敲门)
事件到达#child,触发#child的监听函数 - 冒泡阶段(声音从房间→客厅→小区)
事件从#child→#parent→<body>→window
// 捕获阶段监听
document.getElementById('parent').addEventListener('click', () => {
console.log('parent capture');
}, true)
// 冒泡阶段监听
document.getElementById('child').addEventListener('click', () => {
console.log('child bubble');
}, false)
- addEventListener的三个参数是:事件类型(如'click')、事件处理函数和一个布尔值(true表示捕获阶段,false表示冒泡阶段,默认false)。
输出顺序:
点击child时控制台输出:
parent capture
child bubble
二、事件机制详解:注册与触发的“暗号游戏”
2.1 事件监听的三种方式
事件监听就像给元素设置“暗号”,当特定动作发生时触发回调函数。
| 方式 | 示例 | 特点 |
|---|---|---|
| DOM0级 | element.onclick = fn | 会被覆盖,不推荐 |
| DOM2级 | addEventListener | 支持多监听器 |
| 事件委托 | 父元素监听子元素事件 | 性能优化神器 |
// DOM0级(不推荐)
document.getElementById('child').onclick = () => {
console.log('DOM0级事件');
}
// DOM2级(推荐)
document.getElementById('child').addEventListener('click', () => {
console.log('DOM2级事件');
})
2.2 事件异步执行的“时间差”
事件回调函数不会立即执行,而是放入任务队列等待:
console.log('同步代码'); // 立即执行
setTimeout(() => {
console.log('宏任务'); // 事件队列等待
}, 0)
Promise.resolve().then(() => {
console.log('微任务'); // 优先于宏任务
})
输出顺序:
同步代码
微任务
宏任务
三、addEventListener 的“三个参数”奥秘
3.1 核心语法
addEventListener是现代浏览器推荐的事件绑定方式:
element.addEventListener(
'click', // 事件类型
(event) => { // 回调函数
console.log(event.target);
},
false // useCapture(默认false)
)
3.2 useCapture 参数实战
通过useCapture参数控制事件监听的阶段:
// 捕获阶段监听
document.getElementById('parent').addEventListener('click', () => {
console.log('捕获阶段');
}, true)
// 冒泡阶段监听
document.getElementById('child').addEventListener('click', () => {
console.log('冒泡阶段');
}, false)
点击顺序:
捕获阶段 → 冒泡阶段
四、事件委托:性能优化的“终极武器”
4.1 传统方式 vs 事件委托
为每个子元素绑定监听器 vs 在父元素统一监听:
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
❌ 传统方式(内存爆炸)
const lis = document.querySelectorAll('#list li');
for (let i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', (e) => {
console.log(e.target.innerHTML);
})
}
✅ 事件委托(推荐)
document.getElementById('list').addEventListener('click', (e) => {
console.log(e.target.innerHTML); // 动态元素自动生效
});
- event.target指向触发事件的原始DOM元素,无论事件是否通过冒泡传播,它始终指向最初触发事件的那个元素。
总结
传统方式逐个为DOM元素添加事件监听器(如element.addEventListener)会导致内存爆炸,因为即使元素被移除,监听器仍会因引用关系滞留内存,形成内存泄漏。当元素数量庞大时,每个监听器的内存开销会累积,显著增加页面负担。相比之下,事件委托通过在父元素统一监听事件(利用事件冒泡机制),仅需一个监听器即可管理所有子元素的事件响应,不仅减少内存占用,还能自动适配动态添加/移除的元素,是高效且可维护的解决方案。
4.2 动态元素的“自适应”特性
新增的元素无需重新绑定监听器:
// 新增的<li>无需重新绑定
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log('动态元素被点击:', e.target.innerHTML);
}
});
五、事件对象 event 的“隐藏技能”
5.1 常用属性
事件对象event包含了事件的详细信息:
document.getElementById('child').addEventListener('click', (e) => {
console.log('event.target:', e.target); // 被点击的具体元素
console.log('event.currentTarget:', e.currentTarget); // 绑定监听的元素
e.stopPropagation(); // 阻止事件传播
e.preventDefault(); // 阻止默认行为(如链接跳转)
});
5.2 阻止冒泡的“实战案例”
通过stopPropagation防止事件冒泡到父元素:
<div id="parent" onclick="alert('父元素')">
<div id="child"></div>
</div>
<script>
document.getElementById('child').addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡到父元素
console.log('子元素被点击');
});
</script>
- stopPropagation() 是事件对象的方法,用于阻止事件继续向上传播(冒泡)或向下传播(捕获),防止事件触发父元素或祖先元素的监听器。
六、常见问题与解决方案
6.1 事件冒泡导致的“误触发”
点击按钮时同时触发父元素事件:
<button onclick="handleClick()">点击我</button>
<script>
function handleClick() {
console.log('按钮点击');
}
document.body.addEventListener('click', () => {
console.log('body点击');
});
</script>
现象:点击按钮时输出两行日志
解决方案:在按钮事件中调用e.stopPropagation()
6.2 事件委托的“类型判断”
通过classList判断具体元素类型:
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.classList.contains('special')) {
console.log('特殊元素被点击');
}
});
七、进阶技巧:事件循环与性能优化
7.1 事件循环的“优先级规则”
理解微任务和宏任务的执行顺序:
console.log('Start');
setTimeout(() => {
console.log('Timeout'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('Promise'); // 微任务
});
console.log('End');
输出顺序:
Start → End → Promise → Timeout
7.2 事件节流与防抖
控制高频事件的触发频率:
// 防抖(搜索框输入)
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 节流(窗口调整)
function throttle(fn, delay) {
let flag = true;
return (...args) => {
if (!flag) return;
flag = false;
setTimeout(() => {
fn(...args);
flag = true;
}, delay);
};
}
八、总结:事件机制的“黄金法则”
-
事件流顺序:捕获 → 目标 → 冒泡
-
推荐监听方式:
addEventListener+ 事件委托 -
性能优化技巧:
- 使用事件委托减少监听器数量
- 合理使用
stopPropagation防止冒泡
附机制图一张:
🚀 实战建议:
下次遇到动态列表时,优先使用事件委托;
在调试事件时,打印event.target和event.currentTarget辅助定位;
用Chrome开发者工具的“Event Listeners”面板查看元素绑定的事件。