普通视图

发现新文章,点击刷新页面。
昨天以前首页

事件委托的深层逻辑:当冒泡不够时⁉️

2025年8月17日 13:15

前言

在项目不断扩大之时,管理用户交互变的越来越重要,为每个交互元素附加一个事件监听器是一种糟糕的做法,因为它会导致代码混乱、内存消耗增加以及性能瓶颈。这时,事件委托就派上用场了。

认识dom事件传播

三个阶段

当事件在 DOM 元素上触发时,它不会简单地到达目标并停止。相反,它会经历以下阶段:

  1. 捕获阶段旅程从window级别开始,沿着 DOM 树向下移动,经过每个祖先元素,直到到达目标的父级。带有(中的第三个参数)的事件监听器在此触发useCapture = true``addEventListener
  2. 目标阶段在此阶段,事件到达预期的目标元素。所有直接附加到此元素的监听器都会被触发
  3. 冒泡阶段命中目标后,事件会沿着 DOM 向上“冒泡”,从目标的父元素到祖父元素,依此类推,直到到达目标window。默认情况下,大多数事件监听器都在此阶段运行

事件在dom树中流动过程

< div id = "grandparent" >
< div id = "parent" >
< button id = "child" >点击我</ button > 
</ div > 
</ div > 

如果您单击,则事件流程如下:<button id="child"> click

  1. 捕获 -window -> document -> <html> -> <body> -> <div id="grandparent"> -> <div id="parent">
  2. 目标<button id="child">
  3. 冒泡 - <button id="child"> -> <div id="parent"> -> <div id="grandparent"> -> <body> -> <html> ->document -> window

什么是事件委托

事件委托是一种将事件监听器添加到多个子元素的父元素上,而不是分别添加到每个子元素上的方法。当子元素上发生事件时,它会触发父元素上的监听器,父元素会检查是哪个子元素触发了该事件。

假设一个<ul>包含<li>以下项目的简单列表:

< ul id = "myList" > 
< li >项目 1 </ li > 
< li >项目 2 </ li >
< li >项目 3 </ li >
< li >项目 4 </ li > 
</ ul > 
  

而不是为每个添加一个点击监听器<li>

const listItems = document . querySelectorAll ( '#myList li' ); 
listItems . forEach ( item => { 
  item . addEventListener ( 'click' , ( event ) => { console . log ( `点击于: $ { event . target . textContent } ` );
  });
 });           

通过事件委托,你可以将一个监听器附加到<ul>父级:

onst myList =文档. getElementById ( 'myList' ); 

myList . addEventListener ( 'click' , ( event ) => { 
// 检查点击的元素是否为 <li>
if ( event . target . tagName === 'LI' ) {
console . log ( ` Clicked on : $ { event . target . textContent } ` );
} 
});   
  

在此示例中,当<li>点击任意一个时,click事件都会冒泡到。然后,myList上的单个事件监听器会检查是否是触发了该事件,并采取相应的措施:myList``event.target.tagName``<li>

为什么事件委托如此重要

  • 无需添加数百或数千个监听器,只需几个父容器就足够了,从而大大减少内存占用
  • 更少的监听器可以提高浏览器整体系统内存的使用率,并减少 JavaScript 引擎在事件管理和调度方面的工作量
  • 它支持动态创建元素,这非常实用。假设在页面加载后(例如,在 API 调用后)<li>添加了新元素,监听器仍然有效。无需重新连接监听器。#myList``#myList

事件委托中常见的误区

event.target vs event.currentTarget
  • event.target 是触发事件的特定元素。
  • event.currentTarget 是事件监听器实际附加到的元素。
stopPropagation ()和stopImmediatePropagation () 
  • event.stopPropagation() – 此方法仅允许事件停止沿 DOM 树向上或向下冒泡或捕获。如果在子元素的事件处理程序中执行此方法,则其祖先元素上的任何委托监听器都将无法访问该事件
  • event.stopImmediatePropagation() 这不stopPropagation()的复制粘贴。它的相似之处仅限于添加了这个效果:它阻止进一步的事件传播,并阻止绑定到同一元素的任何其他监听器被执行。

在某些情况下,它们会破坏委托处理程序,例如:子元素的事件处理程序调用stopPropagation将导致位于 DOM 层次结构中更高层级的任何委托监听器的功能失效。委托监听器将无法接收事件。这对于分析、集中式 UI 逻辑或可访问的自定义控件功能尤其麻烦。

非冒泡事件

最突出的非冒泡事件包括:

  • focus– 当元素获得焦点时触发
  • blur– 当元素失去焦点时触发
  • mouseenter– 当指针进入元素时触发
  • mouseleave– 当指针离开元素时触发

为什么它们不起泡

由于浏览器的工作方式以及过去的兼容性问题,通常无法触发此类事件。focusblur旨在在获得或失去焦点的特定元素上触发,因此不存在冒泡。mouseentermouseleavemouseover 和 mouseout配对(它们会产生冒泡);但是与mouseover 和 mouseout不同,mouseenter 和 mouseleave仅在指针位于元素上(而不是其子元素上)时触发。

对于非冒泡事件只能通过自定义冒泡事件来替代

总结

事件委托通过将单个监听器附加到父元素来简化事件处理。当子元素触发事件时,它会向上冒泡到父元素,从而减少内存占用并简化代码。

这种技术在管理大量相似元素(例如列表项或按钮)时非常有效,尤其是在它们动态生成的情况下。父级监听器无需额外配置即可处理新添加元素的事件。

并非所有事件都会冒泡 focusblurmouseleave 等是例外。对于这些事件,可以用 focusinfocusout 或自定义冒泡事件等替代方法。

❌
❌