先记住这个答案
事件委托常说“给父级绑一次监听”,默认前提是事件会冒泡。focus、blur 不冒泡,父级可改听 focusin、focusout,或在捕获阶段听原事件;mouseenter、mouseleave 也不冒泡,可根据需求用会冒泡的 mouseover、mouseout,但进入内部子元素时它们还会额外触发,不能不加过滤地替代。
focus、blur不冒泡mouseenter、mouseleave不冒泡- 可用
focusin、focusout替代 - 捕获阶段也能观察部分事件
表单字段聚焦时高亮所在行
一张动态表格里有许多输入框,希望当前行高亮。直接在表格上默认监听 focus 收不到子输入框的冒泡事件;改听 focusin 就能从父级处理。
const table = document.querySelector('#editor')
table.addEventListener('focusin', event => {
const row = event.target.closest('tr')
if (row && table.contains(row)) row.classList.add('active')
})
table.addEventListener('focusout', event => {
const row = event.target.closest('tr')
// 焦点仍在同一行的另一个控件上时,保留高亮
if (row && !row.contains(event.relatedTarget)) {
row.classList.remove('active')
}
})新加入的行也能通过父级监听得到聚焦反馈。focusout 的 relatedTarget 是即将获得焦点的元素;它仍在原行内时保留高亮,跨到别行或离开表格才移除。若目标为空,例如焦点移出页面,contains(null) 为假,也会移除高亮。
另一种方法是在祖先注册 { capture: true } 的 focus 监听。它利用捕获路径,不意味着 focus 突然变成了冒泡事件。
鼠标进入事件不能机械替换
mouseenter 表达进入一个元素整体,通常不会像 mouseover 那样沿普通冒泡路径到父级。mouseover 可以委托,但指针从父项移入其内部按钮时也可能再触发。
需要“首次进入某个列表项”时,结合 relatedTarget 判断指针是否原本已经在同一项内。事件类型选错会造成反复打开提示层或重复埋点。
具体做法是先用 event.target.closest() 找到当前列表项,再检查 relatedTarget 是否仍被该项包含。若从项里的图标移到文字,两个目标都在同一项内,不应算重新进入;若从另一项移来,才执行“进入”逻辑。离开时用 mouseout 做相反检查,避免鼠标在内部子元素之间移动时反复关闭浮层。
触摸设备没有与鼠标悬停完全对应的操作,所以重要功能不能只靠 mouseover 或 mouseenter 暴露。悬停提示可以是补充反馈,真正的操作仍应有点击或聚焦后的可用入口。事件委托解决监听位置,不会自动解决输入方式差异。
容易答错的地方
- 以为不冒泡就完全不能在祖先监听
- 捕获阶段可能观察到事件,或者有语义相近的可冒泡替代事件。
- 把
mouseover当成完全等价的mouseenter - 进入子元素时它可能再次触发,需要检查
relatedTarget与业务边界。
面试官还会怎么问?
focus 的委托替代事件是什么?
常用 focusin;离开对应 focusout。
为什么 click 比 focus 更容易委托?
通常 click 会冒泡到父级,默认监听即可接收。
事件设置了 bubbles: false 时能捕获吗?
若事件传播路径经过祖先,捕获监听仍可能观察到;只是默认冒泡监听收不到。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。