先记住这个答案
事件委托依赖事件冒泡,而 scroll、blur、focus、mouseenter/mouseleave、load 等事件默认不冒泡,因此无法像 click 那样在祖先元素上通过冒泡捕获。不过,这些事件大多会经历捕获阶段,所以可以在祖先元素上以 capture 方式监听来模拟委托;focus/blur 也可改用冒泡的 focusin/focusout。
- scroll 不冒泡,无法在祖先节点上冒泡委托
- blur/focus 不冒泡,但捕获阶段可委托
- 用 focusin/focusout 可冒泡替代 focus/blur
冒泡是事件委托的基础
事件委托通常是在祖先元素上绑定监听器,利用事件冒泡阶段从目标元素逐级向上传播的特性,统一处理多个子元素的事件。如果事件对象的 bubbles 属性为 false,事件在到达目标元素后不会继续向上传播,祖先元素的冒泡监听器就不会触发。
浏览器中许多非用户交互事件默认不冒泡:scroll 在 document 上不冒泡,但元素上的 scroll 事件不冒泡;blur、focus 不冒泡;mouseenter、mouseleave 不冒泡;load 不冒泡。这意味着在这些事件上使用传统委托模式会失效。
滚动容器统一监听:一个失效场景
假设有一个聊天窗口,消息列表可滚动,需求是用户滚动到底部时加载更多。若直接在列表的 scroll 事件上绑定处理函数并无问题,但若页面中包含多个独立消息列表,并且希望统一在某个父容器上管理滚动行为,直接对父容器监听 scroll 无法捕获子列表的滚动,因为 scroll 不冒泡。
解决方法是改为在捕获阶段监听:父容器.addEventListener('scroll', handler, true)。由于捕获阶段从根节点向下传播到目标元素,所以父容器能捕获到来自任意子元素的 scroll 事件。但要注意,scroll 事件并不冒泡,却仍会经历捕获阶段,因此可被捕获监听。
区分捕获与不可捕获的边界
对于 focus 和 blur,它们不冒泡,但仍在捕获阶段传播,现代浏览器可以监听到。不过更标准做法是使用 focusin 和 focusout,它们是会冒泡的等效事件。
对于 load 事件,它不冒泡且发生在特定资源上,通常不宜委托。要判断一个原生事件能否冒泡,可临时在监听器中打印 event.bubbles。若为 false,则不能使用冒泡委托,但可尝试捕获阶段监听,并确认该事件是否真的能到达捕获监听器。
容易答错的地方
- 认为 scroll 可以冒泡委托
- 实际上 scroll 事件不会冒泡,但很多人误以为它和 click 一样。在 document 上监听 scroll 能捕获整个页面滚动,是因为滚动发生在 document 上,不是事件冒泡。因此监听子元素的 scroll 必须直接绑定或使用捕获。
- 将 mouseenter 误当成 mouseover 行为
- mouseover 会冒泡,mouseenter 不冒泡。有人用 mouseenter 做委托失败,却改用 mouseover 后成功,因为 mouseover 会在移入子元素时再次触发。两者语义不同,委托 mouseenter 应改用 mouseover 并检查 relatedTarget。
面试官还会怎么问?
blur 事件不冒泡,如何实现表单失焦时触发验证的委托?
可以在容器上监听 focusout(会冒泡)事件,或使用捕获阶段的 blur。focusout 与 blur 触发时机相同,但 focusout 会冒泡,所以适合委托。注意 focusout 事件对象的 relatedTarget 指向新获得焦点的元素。
scroll 事件虽然不冒泡,但可监听捕获阶段,这是否意味着所有非冒泡事件都能用捕获委托?
不一定。非冒泡事件(如 load)在捕获阶段也会传播,但 load 触发时可能已来不及绑定或不符合语义。scroll 的捕获是可靠方式,但并非所有事件都适合捕获委托,需具体判断。
mouseenter 与 mouseover 相比,在委托上有什么替代方案?
mouseenter 不冒泡,可在容器上监听 mouseover,并通过检查 relatedTarget 判断是否真正进入容器或其子元素。mouseover 会频繁触发,需要额外处理。现代浏览器也支持监听 mouseenter 的捕获阶段。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。