
我刚入行那会儿,还是jQuery的时代。当时面试,事件委托(Event Delegation) 几乎是必考题。能用.on()
方法,在一个父元素上,优雅地处理动态添加的子元素的事件,是衡量一个前端是否会玩的重要标准。
但快十年过去了,我们现在天天都在写React和Vue。我们习惯了在JSX或template里,直接给元素绑定onClick
,像这样:
jsx
<button onClick={handleClick}>Click Me</button>
这太方便了,以至于我们几乎忘了事件委托的存在。
那么,问题来了:事件委托这个听起来有点老古董的技术,在2025年的今天,还有用武之地吗?还是说,它已经被现代框架彻底淘汰了?
今天,我想聊聊我的看法。答案可能有点反直觉:
它不仅有用,而且你可能每天都在不知不觉地使用它。
什么是事件委托?
在深入框架之前,我们快速花一分钟,回顾一下什么是事件委托。
事件委托的核心,是利用了浏览器事件模型的 事件冒泡(Event Bubbling)机制。

传统方式 :如果你有一个包含100个<li>
的列表,你需要给每个<li>
都绑定一个点击事件,那就在内存中创建了100个事件监听器。
事件委托 :你只需要在它们的父元素<ul>
上,绑定一个 点击事件。当用户点击任何一个<li>
时,这个点击事件会像气泡一样,冒泡到父元素<ul>
上,被<ul>
的监听器捕获。
它的好处显而易见:
- 性能:内存中只有一个监听器,而不是100个,性能开销小。
- 动态性 :即使你后续通过JS,动态地往
<ul>
里新增了第101个<li>
,这个点击事件依然对它生效,无需重新绑定。
来看一个原生JS的经典实现:
javascript
<ul id="my-list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
javascript
const list = document.getElementById('my-list');
list.addEventListener('click', function(event) {
// event.target 是用户实际点击的那个元素
if (event.target && event.target.matches('li')) {
console.log('Clicked on item:', event.target.textContent);
}
});
可是 React/Vue已经在帮你做事件委托了
好了,回到我们最初的问题。在React/Vue里,我们还需要自己去写上面那样的代码吗?
绝大部分情况下,不需要。因为框架已经做得更好了。

当你像下面这样,给100个按钮都写上onClick
时,你以为React真的在每个DOM按钮上都调用了addEventListener
吗?
jsx
function ButtonList() {
const items = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Button ${i}` }));
return (
<div>
{items.map(item => (
<button key={item.id} onClick={() => console.log(item.id)}>
{item.name}
</button>
))}
</div>
);
}
答案是:没有。React并没有那么傻。
它在底层实现了一套非常强大的 合成事件系统 (Synthetic Event System),而这套系统的核心,就是全局的事件委托。
简单来说,React的工作原理是: 对于绝大部分事件(比如click
, input
, keydown
等),React并不会把事件处理器直接绑定在你写的DOM元素上。
相反,它会在应用的根节点 (比如document
或者#root
),只绑定一个统一的事件监听器。
当你在某个按钮上点击时,这个原生的click
事件会一路冒泡到根节点,被React的统一监听器捕获。然后,React会根据event.target
等信息,去查找你点击的这个DOM元素,对应的是哪个React组件,并找到你写在组件上的onClick
处理器,最后再执行它。
Vue也采用了类似的策略。
所以,事件委托并没有过时。它只是被我们的框架内化了,成为了我们享受到的、高性能体验的幕后。 你写的每一个onClick
,都在享受着事件委托带来的红利,而无需关心其实现细节。
那我们自己,还有必要手写事件委托吗?
既然框架都帮我们做好了,那是不是意味着我们就可以彻底忘记这个技术了?
也不全是。在95%的情况下,你可以完全信赖框架。但在剩下5%的极限或特殊场景下,手动实现事件委托,依然是一个强大的工具。
超高频事件的性能优化
想象一个有数千个单元格的在线表格,或者一个数据可视化应用。每个单元格都需要响应mouseover
事件来显示高亮或Tooltip。
如果你给这几千个单元格,都绑定一个React的onMouseOver
,即使React有事件委托,在合成事件的分发和处理层面,当鼠标快速移动时,也可能会产生一定的性能开销。
在这种极限情况下,我们可以绕过React的合成事件系统,回归原生:
jsx
function HighPerformanceGrid() {
const gridRef = useRef(null);
useEffect(() => {
const gridElement = gridRef.current;
if (!gridElement) return;
const handleMouseOver = (event) => {
// 直接操作DOM,不触发React的re-render
if (event.target && event.target.matches('.grid-cell')) {
event.target.style.backgroundColor = 'lightgray';
}
};
// 手动在父元素上进行事件委托
gridElement.addEventListener('mouseover', handleMouseOver);
return () => {
gridElement.removeEventListener('mouseover', handleMouseOver);
};
}, []);
return <div ref={gridRef}>{/* 渲染成千上万个 .grid-cell 元素 */}</div>;
}
这种方式,性能几乎是最高的,因为它完全是原生DOM和事件的处理,避免了React的虚拟DOM diff和组件渲染流程。
操作不受框架控制的DOM
当你需要在一个React组件里,去管理一个由dangerouslySetInnerHTML
渲染的、或者由某个第三方非React库生成的、不受React控制的DOM结构时,你无法在那些DOM元素上使用onClick
。(尤其是 echarts
, mapbox
等第三方Popup弹窗实现时非常有用)
此时,在这些DOM的父容器上,手动进行事件委托,就是最理所当然、也是唯一的选择。
事件委托是web前端开发的基础技能。
在现代框架的时代,我们可能不再需要天天去手写它。但它能让你更深刻地理解框架的运行原理,也能让你在遇到棘手的性能问题时,多了一个解决办法。
所以,下次当有新人问你事件委托还有没有用时,你可以告诉他:
它不仅有用,而且你可能天天都在用,只是React/Vue在替你负重前行
谢谢大家😁