前言
大家好,这里是JS事件机制讲解的第二篇,之前我们在第一篇中详解了事件流中的捕获阶段、目标阶段、冒泡阶段。这篇文章会仔细讲讲JS原生事件中的委托机制,以及一些事件机制的应用,相信看完这一篇后能够将JS的事件机制理解推向一个全新的高度,达到大厂的水平!
何为事件委托?
请你查看下面的场景
这是一段html代码,我们的父元素myList
里面放着多个子元素li
,它们的自定义属性data-item
的值互不相同。
html
<div id="root">
<ul id="myList">
<li data-item="123">Item 1</li>
<li data-item="456">Item 2</li>
<li data-item="789">Item 3</li>
<li data-item="012">Item 4</li>
</ul>
</div>
现在我们想要为myList
里面的每一个li
元素添加一个效果:点击某个li
元素,会输出它的内容。
请你想想?我们该如何做?
我们是不是应该为每一个li
元素添加一个click的addEventListener。
js
document.querySelectorAll('#myList li').forEach(item => {
item.addEventListener('click', () => {
console.log(item.dataset.item); //输出它的自定义属性
});
});
查看效果:

可以看到这种方式是有效的,确实能够输出每个元素的data-item
让我们结合上一篇的文章的事件流过程进行分析这里的过程:
在这里,事件从 window
→ document
→ #root
→ #myList
-> li
向下捕获,到了目标阶段,会到达每个实际被点击的 li
元素,因此该元素上的事件的监听器会触发,然后事件从 li
→ #myList
→ #root
向上冒泡,依次触发冒泡的元素的监听器。
这种方式性能太差了
但是我们完全没有必要这样做,因为这种为每一个子元素都添加一个事件监听的方式性能很差
接下来,让我们用事件委托来进行优化:
事件委托 : 事件委托 是利用事件冒泡机制,将子元素的事件监听统一委托给父元素处理。通过 event.target
判断实际触发元素,减少内存消耗,提升性能。
所以在这个案例中,我们可以改为仅为父元素myList设置事件监听 ,让它统一处理各个子元素li
的内容输出
js
document.getElementById('myList').addEventListener('click',function(event){
console.log(event.target);
})
这样做的话,当我们点击某个item时 ,在冒泡阶段:
-
从子元素开始,首先没有发现子元素
li
的事件监听,继续往上冒泡 -
冒泡到父元素
myList
,发现其有事件监听,于是执行它的事件函数:打印event.target
。
此时: 这里的event.target始终指向最初被点击的<li>
,就是我们点击的某个具体元素

所以利用这种事件委托的方式,同样可以达到我们想要的效果,并且它的性能大大提升:
- 仅需一个监听器,就可以达到传统使用N个监听器同样的效果
- 内存占用恒定,不随子元素数量增加
同时,而且如果想要对某个元素进行限定,可以利用下面这种方式
js
document.getElementById('myList').addEventListener('click',function(event){
console.log(event.target.dataset.item);
// 对456这个元素进行限定
if (event.target.dataset.item === '456'){
console.log('Im Item2!!!')
}
})

事件委托的必要条件:唯一值
在事件委托中,对于各个子元素,我们需要为每个子元素设置一个唯一值,如同上面的item1
->item4
,你可以看见我们设置了自定义属性data-item
为"123"、"456"、...这样,必须这样设置唯一值的原因如下:
1. 精确识别目标元素
当使用事件委托时,事件处理程序是绑定在父元素上的,但我们需要知道事件实际发生在哪个子元素上。唯一值可以帮助我们:
- 区分不同的子元素
- 精确识别用户实际交互的元素
- 避免处理不相关的子元素事件
2. 性能优化
- 避免为每个子元素单独绑定事件处理程序
- 通过唯一值可以快速查找和定位目标元素
- 减少内存使用(只需一个事件处理程序)
事件委托的就近原则
每个事件委托中,理论上,子元素不仅可以委托到父元素,还可以委托到父元素的父元素,比如li的父元素为myList,body同样也可以称为li的父元素,由于冒泡阶段会一直往外冒泡,所以上面的例子我们选择body或者document都是没有问题的,当选择了某个父元素时,我们称这个父元素代理了子元素,父子元素共同形成了一个事件委托
但是事件委托有个原则--就近原则:
理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,以提高性能。比如推荐在myList
上代理li
,而不是在document
上代理li
。
事件委托应用:动态添加元素
让我们对刚才的案例进行变更:
现在,我们想要在页面上添加一个按钮,用户点击按钮时能够增加一个myList
里面的元素li
html
<div id="root">
<ul id="myList">
<li data-item="123">Item 1</li>
<li data-item="456">Item 2</li>
<li data-item="789">Item 3</li>
<li data-item="012">Item 4</li>
</ul>
<div id="container" data-item="ab1">hello</div>
<button id="btn">添加节点</button>
</div>
我们需要实现:对于新添加的li
,点击后会输出这个li
的相关内容。
在之前,你可能会说,直接新添一个元素并且对这个新元素注册一个事件click不就好了呗
但是今天在学了委托机制后,你是否有了新的思路呢?
我们完全可以仅为父元素注册一个事件监听就可以完成上述效果!
js
document.getElementById('myList').addEventListener('click',function(event){
console.log(event.target.innerText)
})
document.getElementById('btn').addEventListener('click',function(event){
// 动态添加元素
const newLi = document.createElement('li');
newLi.appendChild(
document.createTextNode('item-new')
)
document.getElementById('myList').appendChild(
newLi
)
})

因此,我们只需要对该按钮绑定一个添加元素的事件即可,对新添加的元素,无需注册事件。 这就是事件委托在动态添加元素上的应用
事件委托应用:Toggle Menu
对于这个案例,先给大家看看我们想要实现的效果:
- 点击Toggle Menu控制menu的显示开关
2.点击页面任意空白处可以关闭menu的显示

这是一种多见的设计,我们平常在上网时,会经常见到这种设计,举个例子,比如掘金
点击发布按钮可以控制发布菜单的展开与收回,点击页面任意处可以收回已展开的发布菜单
接下来,让我们来实现这种效果:
1.写html和css
html
<style>
#menu {
position: absolute;
top: 50px;
left: 50px;
padding: 20px;
background-color: #f2f2f2;
border: 1px solid #ccc
}
#toggleBtn {
cursor: pointer;
}
</style>
<div id="toggleBtn">Toggle Menu</div>
<div id="menu">
<p>Menu Context</p>
<a href="https://www.baidu.com" id="closeInside" style="display: none">Don't closed</button>
</div>
可以看到,默认这个menu组件是不显示的(display: none)
2.书写js
在js中,首先我们先实现:
点击tooggleBtn时可以控制menu的控制与收回,注册一个事件即可
js
const toggleBtn = document.getElementById('toggleBtn')
const menu = document.getElementById('menu')
const closeInside =document.getElementById('closeInside')
toggleBtn.addEventListener('click',function(e){
menu.style.display = menu.style.display === 'block'?'none':'block'
})
现在就可以通过点击Toggle Menu这行文字控制menu的显示与收回了
接下来我们要实现:
点击页面任意空白处能够将已显示的menu给收回
我们这样做:
js
document.addEventListener('click',function(){
menu.style.display = 'none'
})
但是这个时候你会发现,点击Toggle Menu后,menu打不开了!
这是因为:
在冒泡阶段 从里向外冒泡,先执行里面的这个事件menu.style.display = menu.style.display === 'block'?'none':'block'
,然后再执行外面的menu.style.display = 'none'
事件,外面的事件后执行,所以无论如何menu都不会显示!!!
所以我们这种写法有误,正确的写法应该是此时为子元素添加e.stopPropagation()
js
//修改后,添加e.stopPropagation()
toggleBtn.addEventListener('click',function(e){
e.stopPropagation()
menu.style.display = menu.style.display === 'block'?'none':'block'
})
请问e.stopPropagation()
是什么?有什么作用?
e.stopPropagation()
是 JavaScript 中用于处理事件冒泡的方法,e.stopPropagation()
的作用是阻止事件继续向上冒泡,即事件只会触发当前元素的处理函数,而不会传播到其父元素。
所以我们这里用e.stopPropagation()
能够很好地完成效果:点击子元素时,会阻止父元素的事件发生,只执行子元素的事件!
这种写法是一种常见的最佳实战,对于不想冒泡到外面的子元素,可以使用这种写法轻松实现,而且也不会影响父元素事件本身的执行。
所以这就是事件机制在这种Toggle Menus上的应用!
拓展:现代react的合成事件SyntheticEvent
上面我们介绍了的是JS原生的事件机制,下面介绍下React的事件机制,React 的事件机制是基于浏览器原生事件系统的封装,但进行了优化和跨浏览器兼容性处理。以下是 React 事件机制的核心特点和工作原理:
1. 合成事件(SyntheticEvent)
React 使用合成事件(SyntheticEvent) 统一不同浏览器的原生事件差异。所有事件都是通过SyntheticEvent
的实例传递的,它是浏览器原生事件的跨浏览器包装器,具有与原生事件相同的接口(如stopPropagation()
、preventDefault()
),但行为一致。
js
<button onClick={(e) => {
e.preventDefault(); // 合成事件的方法
console.log(e.nativeEvent); // 访问底层原生事件
}}>点击</button>
2. 事件委托(Event Delegation)
React 会自动帮我们实现事件委托:它不会将事件直接绑定到具体的 DOM 节点,而是通过事件委托 将所有事件统一绑定到document
(React 17 之前)或根 DOM 节点(React 17+)。这样减少了内存消耗,并能动态处理新增/删除的组件事件。
- React 16及之前 :事件委托到
document
。 - React 17+ :事件委托到
ReactDOM.render
绑定的根节点(如root
),避免多版本 React 共存时的问题。
3. 事件池(Event Pooling)
React 17 之前,合成事件对象会被放入事件池 复用,事件回调执行后,事件对象的属性会被清空。若需异步访问事件(如setTimeout
),需调用e.persist()
保留事件。
javascript
// React 16 及之前需要 e.persist()
<button onClick={(e) => {
e.persist(); // 保留事件对象
setTimeout(() => console.log(e.target), 100);
}}>Click</button>
React 17+ 移除了事件池机制 ,无需再调用e.persist()
。
4. 事件命名规则
React 事件使用驼峰命名 (如onClick
、onMouseDown
),而非原生 DOM 的小写(如onclick
)。
总结:React 事件机制的优势
- 跨浏览器一致性 :通过
SyntheticEvent
屏蔽浏览器差异。 - 性能优化:事件委托减少内存占用。
- 声明式语法:与组件生命周期无缝集成。
理解这些机制有助于避免常见陷阱(如异步访问事件对象、this
绑定问题等)。
总结
以上就是JS事件机制的全部内容了,通过这两篇事件机制详解的文章,我们可以对JS的事件机制做一个全面的认识与掌握,最后还简单下介绍了React的现代合成事件,它是如此的震撼!如果对React的合成事件机制感兴趣的话,可以去阅读React的一些官方文档,这篇文章系列主要是介绍JS原生的事件机制,作者后期可能也会更新React的合成事件机制,如果感兴趣的话,可以点个关注呀,谢谢大家!
