事件委托应绑定父节点并用e.target.closest()定位目标,避免为大量子元素重复绑定;focus/blur不可委托,需用focusin/focusout替代;委托不适用于子节点极少或行为差异大的场景。

直接给父节点绑一次 click 监听器,用 e.target.closest() 定位真实点击项,就能稳稳撑住成千上万个子元素——不是“理论上可以”,而是生产环境里被验证过的真实做法。
为什么不能逐个绑定 click 事件
给 5000 个 li 各自调用 addEventListener('click', handler),会创建 5000 个独立函数实例和监听器对象。这不只是内存涨得快,更关键的是:DOM 插入新节点后,这些新节点完全不响应,还得再补一次绑定逻辑;重排重绘也更容易被频繁触发。
- 每个监听器至少占用几百字节内存,5000 个就是 MB 级别堆压力
- 初始化阶段遍历 DOM + 绑定,明显拖慢首屏交互就绪时间
- 动态插入的子节点(比如分页加载、搜索过滤)默认无事件响应
必须用 closest() 而不是 matches() 或 tagName 判断
e.target.matches('.item') 看起来简洁,但只要用户点的是 .item 里的 span、i 或 button,就匹配失败;e.target.tagName === 'LI' 同理,嵌套结构下极易漏判。而 e.target.closest('.item') 会从点击的实际元素开始向上找,直到命中第一个匹配的祖先或自身,语义准确、容错性强。
-
closest()在 IE11+ 和所有现代浏览器中稳定支持 - 它天然处理了“点在图标上、但想操作整行”的常见 UI 场景
- 避免手写多层
parentNode遍历或正则匹配 class 字符串
父容器选哪里?别碰 document 或 body
把监听器挂到 document 上看似一劳永逸,实则埋雷:事件路径变长,判断逻辑要额外过滤无关区域;其他模块也可能监听同类型事件,互相干扰;且无法利用 CSS 作用域隔离,容易误响应弹窗、侧边栏里的同类选择器。
- 优先选最靠近子元素、结构稳定的直接父容器,比如
ul.list、div.table-body - 确保该容器不会被 JS 全量替换(如
innerHTML = ''),否则监听器丢失 - 若容器本身是动态渲染的(如 Vue 的
v-if),需在挂载后重新绑定,或改用框架级委托(如 Vue 的@click.stop配合closest)
哪些事件根本不能委托?focus / blur 就是典型
focus 和 blur 默认不冒泡,你在父节点上监听永远收不到。这不是写法问题,是浏览器规范决定的。真要实现类似效果,得换用 focusin / focusout——它们是冒泡版替代品,行为一致,兼容性也足够(Chrome 58+、Firefox 52+、Safari 11.1+、Edge 79+)。
-
mouseenter/mouseleave同样不冒泡,但通常不需要委托——它们本就是为“进入/离开整个区域”设计的 - 自定义事件若需委托,构造时必须显式传
{ bubbles: true } - 表单提交类事件(
submit)可委托,但注意e.preventDefault()要在委托函数里调用,而非子节点上
真正难的不是写对那几行代码,而是判断“这个场景到底值不值得委托”:子节点少于 10 个?行为差异极大?需要精确控制 this 指向?这时候硬套委托反而让逻辑更绕、更难 debug。










