事件委托(Event Delegation)的原理

优化前的问题

原来的代码是这样的:

TSX 复制代码
{allCategories.map((category) => (
  <div
    key={category.id}
    className={styles.categoryItem}
    onClick={() => onCategoryChange(category.slug)}  // ❌ 每个 item 都创建新函数
  >
    {/* ... */}
  </div>
))}

问题分析:

  1. 每次渲染都会为每个分类项创建一个新的箭头函数:() => onCategoryChange(category.slug)
  1. 假设有 20 个分类,就会创建 20 个函数,这些函数会形成闭包,占用内存
  1. 每次组件重新渲染时,React 会认为 onClick 是一个新的函数引用,可能导致不必要的子组件重新渲染
  1. 虽然通常不是性能瓶颈,但在分类数量很多时,这会造成不必要的开销

二、事件委托(Event Delegation)的原理

事件委托将事件处理函数绑定到父容器上,而不是每个子元素。利用事件冒泡,点击子元素时,事件会冒泡到父容器,父容器的事件处理函数接收事件,并根据目标元素执行相应逻辑。事件冒泡示意图:

TEXT 复制代码
点击子元素 → 事件冒泡 → 父容器捕获事件 → 根据 target 判断处理逻辑

三、优化后的实现

1. 在父容器上绑定统一的事件处理函数

TSX 复制代码
 // 处理分类列表点击 - 使用事件委托
  const handleCategoryListClick = (e: React.MouseEvent<HTMLDivElement>) => {
    const target = e.target as HTMLElement;
    // 向上查找最近的 categoryItem 元素
    const categoryItem = target.closest(`.${styles.categoryItem}`);
    if (categoryItem) {
      const categorySlug = categoryItem.getAttribute('data-category-slug');
      if (categorySlug) {
        onCategoryChange(categorySlug);
      }
    }
  };

函数执行流程:

  1. e.target 获取被点击的具体元素(可能是图标、文字等)
  1. closest() 向上查找最近的 .categoryItem 元素
  1. 从该元素读取 data-category-slug 属性
  1. 调用 onCategoryChange(categorySlug)

2. 在父容器上绑定事件

HTML 复制代码
      <div className={styles.categoryList} onClick={handleCategoryListClick}>

3. 为每个 item 添加 data 属性

ini 复制代码
  data-category-slug={category.slug}

使用 data-* 属性存储分类标识,符合 HTML5 规范。

四、代码执行流程示例

假设用户点击了"热门工具"分类项:

TEXT 复制代码
1. 用户点击 "热门工具" 文字
   ↓
2. 事件冒泡到 <div className={styles.categoryList}>
   ↓
3. handleCategoryListClick 函数被触发
   ↓
4. e.target = <span className={styles.categoryName}>热门工具</span>
   ↓
5. target.closest('.categoryItem') 向上查找
   → 找到 <div className={styles.categoryItem} data-category-slug="hot">
   ↓
6. categoryItem.getAttribute('data-category-slug') 
   → 返回 "hot"
   ↓
7. 调用 onCategoryChange("hot")
   ↓
8. 完成分类切换

五、为什么使用 closest()?

因为点击可能发生在 item 内部的任意元素上(图标、文字等)。closest() 可以向上查找最近的匹配元素:

HTML 复制代码
<div className={styles.categoryItem} data-category-slug="hot">
  <div className={styles.categoryContent}>
    <div className={styles.categoryIcon}>ICON</div>  // ← 可能点击这里
    <span>热门工具</span>                          // ← 也可能点击这里
  </div>
</div>

无论点击图标还是文字,closest() 都能找到外层的 categoryItem。

### 六、其他优化方案的对比

  1. 创建单独的子组件 + useCallback
  • 优点:封装性好
  • 缺点:增加了组件层级和复杂度
相关推荐
学嵌入式的小杨同学4 小时前
从零打造 Linux 终端 MP3 播放器!用 C 语言实现音乐自由
linux·c语言·开发语言·前端·vscode·ci/cd·vim
weixin_425543735 小时前
TRAE CN3.3.25 构建的Electron简易DEMO应用
前端·typescript·electron·vite·nestjs
Mr Xu_6 小时前
【Vue3 + ECharts 实战】正确使用 showLoading、resize 与 dispose 避免内存泄漏
前端·信息可视化·vue·echarts
0思必得06 小时前
[Web自动化] Selenium设置相关执行文件路径
前端·爬虫·python·selenium·自动化
雯0609~6 小时前
hiprint:实现项目部署与打印1-官网提供普通html版本
前端·html
不绝1917 小时前
UGUI——进阶篇
前端
Exquisite.7 小时前
企业高性能web服务器(4)
运维·服务器·前端·网络·mysql
2501_944525548 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 账户详情页面
android·java·开发语言·前端·javascript·flutter
2601_949857438 小时前
Flutter for OpenHarmony Web开发助手App实战:快捷键参考
前端·flutter
wangdaoyin20108 小时前
若依vue2前后端分离集成flowable
开发语言·前端·javascript