引入
今天在探索JavaScript事件机制时,我仿佛闯入了DOM世界的魔法学院。这里的事件就像调皮的魔法精灵,它们的行为规律让我大开眼界!先来看看这个神奇的现象:
xml
<!-- 魔法盒子实验 -->
<div id="parent" style="background:blue;width:200px;height:200px;">
<div id="child" style="background:red;width:100px;height:100px"></div>
</div>
<script>
parent.addEventListener('click', () => console.log('父元素被点啦!'), false)
child.addEventListener('click', () => console.log('子元素被戳啦!'), false)
</script>
当我点击红色子盒子时,控制台居然依次打印:

咦?明明只点了子元素,为什么父元素也"感觉"到了?原来这就是事件冒泡的魔法效果!事件就像水中的气泡,从触发点(子元素)慢慢浮到顶层(父元素)。不过别急,这只是故事的一半...
事件的三段奇妙旅程
DOM世界的事件传播就像一场精心编排的舞台剧,分为三个精彩幕次:
- 捕获阶段:事件从window逐级向下"潜行"到目标元素
- 目标阶段:事件到达目标元素
- 冒泡阶段:事件从目标元素向上"浮出"到window

当我们使用addEventListener
时,第三个参数就是控制魔法时机的开关: 我们可以看MDN的官方文档怎么介绍addEventListener
true
:在捕获阶段触发(魔法自上而下)false
(默认):在冒泡阶段触发(魔法自下而上)
通俗来说,我们当用true
会先触发父元素事件,再到子元素事件,当是false
则相反,当然这是建立在父元素和子元素绑定了同一个事件,且在子元素身上触发了
结论:
addEventListener
第3个参数决定了事件是在捕获阶段触发还是在冒泡阶段触发addEventListener
第3个参数为true
表示捕获阶段触发,false
表示冒泡阶段触发,默认值为false
- 事件流只会在父子元素具有相同事件类型时才会产生影响
- 绝大部分场景都采用默认的冒泡模式(其中一个原因是早期 IE 不支持捕获)
阻止冒泡e.stopPropagation()
事件对象.stopPropagation()
阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。
此方法可以阻断事件流动传播,不光在冒泡阶段有效,捕获阶段也有效
xml
<body>
<div class="father">
<div class="son"></div>
</div>
<script>
const father = document.querySelector('.father')
const son = document.querySelector('.son')
document.addEventListener('click', function () {
alert('我是爷爷')
})
father.addEventListener('click', function () {
alert('我是爸爸')
})
son.addEventListener('click', function (e) {
alert('我是儿子')
e.stopPropagation() //阻止冒泡
})
</script>
</body>
我们来看效果:
可以看到当我们点击子元素,是没有触发祖先事件的,这就是
e.stopPropagation()
的作用
结论:
事件对象中的 ev.stopPropagation
方法,专门用来阻止事件冒泡。
鼠标经过事件:
mouseover 和 mouseout 会有冒泡效果
mouseenter 和 mouseleave 没有冒泡效果 (推荐)
DOM0 vs DOM2:魔法咒语的进化史
在魔法学院的历史上,曾有两种施法方式:
xml
<!-- DOM0 古早咒语(已过时) -->
<a onclick="doSomething()">点我试试</a>
<!-- DOM2 现代魔法 -->
element.addEventListener('click', doSomething)
为什么现代巫师都用DOM2呢?因为DOM0有三个致命缺陷:
- 只能绑定一个处理函数(最后覆盖前面的)
- HTML和JS耦合严重(违反职责分离原则)
- 无法控制事件传播阶段
而DOM2的addEventListener
就像多功能魔法杖:
- 可绑定多个处理函数
- 可精确控制捕获/冒泡阶段
- 代码可维护性更高
DOM2级事件实现了模块化分离,我们就应该html,css,js 分离来写,这样的代码可读性是十分优雅的
事件委托:以一敌百的智慧
当我遇到这个需求时:
"给列表中所有li
添加点击事件新手巫师可能会这样:
ini
const lis = document.querySelectorAll('li');
lis.forEach(li => {
li.addEventListener('click', handleClick);
});
我们的浏览器是存在一个event loop,我们这样操作就是注册很多事件,每一个事件都存在监听器
当列表有1000项时,相当于创建了1000个事件监听器!这就像雇佣1000个卫兵每人看守一颗石子------效率低下到让国王破产。
智慧的老巫师微微一笑,祭出了事件委托大法:
xml
<ul id="myList">
<li>item1</li>
<li>item2</li>
<!-- 更多li... -->
</ul>
<script>
document.getElementById('myList').addEventListener('click', e => {
if (e.target.tagName === 'LI') {
console.log(`你点击了:${e.target.textContent}`);
}
});
</script>
这里的神奇之处在于:
- 只在父元素
ul
上设置一个监听器 - 通过
e.target
识别实际点击的元素 - 自动处理动态添加的子元素(无需重新绑定)
这就像在城堡门口设一个智能安检门,自动识别并处理不同人员,而不是给每个房间都派卫兵!
事件委托是利用事件流的特征解决一些现实开发需求的知识技巧,主要的作用是提升程序效率。
利用事件流的特征,可以对上述的代码进行优化,事件的的冒泡模式总是会将事件流向其父元素的,如果父元素监听了相同的事件类型,那么父元素的事件就会被触发并执行,正是利用这一特征对上述代码进行优化,
我们来看效果,一样可以满足我们的需求

target vs currentTarget:魔法镜的奥秘
在事件委托中,理解这两个属性至关重要:
- e.target:实际触发事件的元素(事件起源)
- e.currentTarget:当前处理事件的元素(绑定监听器的元素)
javascript
myList.addEventListener('click', function(e) {
console.log(e.target); // 被点击的<li>
console.log(e.currentTarget); // 永远是<ul>
});
这就像快递配送:
- target是原始发货人(工厂)
- currentTarget是当前中转站(配送中心)
React中的魔法优化
在React王国里,事件委托被用到了极致。所有的React事件都委托到 #root容器:
javascript
// React内部相当于这样做了:
document.getElementById('root').addEventListener('click', e => {
// 通过虚拟DOM找到对应组件处理
});
这种设计带来三大优势:
- 内存高效:整个应用共用少量监听器
- 动态无忧:组件动态增减不影响事件
- 一致行为:统一处理事件冒泡逻辑
结语:事件机制的哲学
从蓝色父盒子与红色子盒子的互动,到高效处理动态列表,事件机制教会我们编程的重要哲学:
优秀的代码不是控制每个个体,而是建立优雅的响应系统
就像DOM世界的事件流,我们的人生也充满各种"事件"。理解它们的传播机制,学会用"委托"智慧处理问题,才能在代码与生活的复杂性中游刃有余。