优化前的问题
原来的代码是这样的:
TSX
{allCategories.map((category) => (
<div
key={category.id}
className={styles.categoryItem}
onClick={() => onCategoryChange(category.slug)} // ❌ 每个 item 都创建新函数
>
{/* ... */}
</div>
))}
问题分析:
- 每次渲染都会为每个分类项创建一个新的箭头函数:() => onCategoryChange(category.slug)
- 假设有 20 个分类,就会创建 20 个函数,这些函数会形成闭包,占用内存
- 每次组件重新渲染时,React 会认为 onClick 是一个新的函数引用,可能导致不必要的子组件重新渲染
- 虽然通常不是性能瓶颈,但在分类数量很多时,这会造成不必要的开销
二、事件委托(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);
}
}
};
函数执行流程:
- e.target 获取被点击的具体元素(可能是图标、文字等)
- closest() 向上查找最近的 .categoryItem 元素
- 从该元素读取 data-category-slug 属性
- 调用 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。
### 六、其他优化方案的对比
- 创建单独的子组件 + useCallback
- 优点:封装性好
- 缺点:增加了组件层级和复杂度