事件的捕获和冒泡
DOM 事件模型包括捕获和冒泡,捕获是从上往下到达目标元素,冒泡是从当前元素,也就是目标元素往上到 window。
如下图所示, DOM事件(专指 DOM2 级事件)包含以下三个阶段
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
第二个阶段其实可以归属于冒泡阶段,这里先这么分。并不是所有浏览器的事件体系都遵循这三个阶段,比如一些旧版本的 IE 并不支持事件捕获。相对来说,事件冒泡的兼容性更好些,基本所有现代浏览器都支持事件冒泡。
由于事件可以冒泡,那么事件就可以放在最上层的元素中处理(React 就是放在 document 中处理),这个过程叫做事件委托,事件委托是我们编写 js 代码常用到的手段,但是有些事件比较特殊,常常导致事件委托失效,产生一些千奇百怪的问题。这些问题的实质是由于有些事件没有进行冒泡导致的,我们今天就来看看这些常用却又特殊的事件。
注意,以下的所有示例代码运行在 chrome 浏览器,版本是 83。
scroll
scroll 事件在元素滚动条在滚动时触发。我们假设有如下 html 结构.
<
最外层元素的 id 是 container。内部的第一个元素 id 是 outer,outer 的高度要高于 caontainer。outer 内部的元素 id 是 inner,inner 的高度高于 outer。
这样的话,outer 和 container 上就会存在滚动条,也就是这两个元素都是有 scroll 事件的。
我们通过如下代码为 container 和 outer 添加 scroll 监听事件。
document
注意,这里 addEventListener 的第三个参数是 false,也就是 useCapture 为 false。如果 scroll 事件可以冒泡的话,当触发 outer scroll 的时候,事件就会冒泡到 container,触发 container scroll。
当我们在 inner 区域滚动,实际的效果是。
outer
outer 滚动的时候事件并没有冒泡到 container 区域。这说明了 scroll 事件是不会冒泡的。
现在我们换一种方式添加监控。
document
此时的 useCapture 为 true,同样是在 inner 上向下滚动。控制台显示如下:
container
这符合我们对事件捕获的预期,scroll 事件捕获的方向从 container 到 outer,依次触发回调方法。
scroll 事件的特殊性除了不会进行冒泡这一点外,还有一点,那就是 scroll 事件的无法取消的,使用 stopPropagation 或者是 preventDefault 都无法阻止 scroll 事件,这也很好理解,scroll 连冒泡都没有了,阻止冒泡传播当然也含无意义。。
e
那如何取消 scroll 事件呢,其实这个问题就是个伪命题,因为先有滚动才有滚动事件。想要阻止滚动,必须在事件发生之前就阻止,我们一般的做法是阻止 wheel 和 touchstart 的默认动作。
document
此时用滚轮操作事件就会失效,outer 区域无法滚动,当然,这种方法并不能禁用滚动条,我们还需要禁用滚动条的拖动事件。
上述的方法是不是很麻烦,所以这种方法我们一般用不到,我们有更常用也更简单的方式,直接修改样式。
overflow:hidden
scroll 事件总结
- scroll 事件不会冒泡,这个带来的影响就是,当我们去做事件委托的时候,其它的大部分事件可以在冒泡阶段的时候完成委托,而 scroll 事件必须在捕获阶段完成委托。
- scroll 事件无法取消( 没有冒泡的基本都没法取消 ),scroll 回调中的 preventDefault 和 stopPropagation 都是无效的。
blur & focus
和 scroll 事件一样,focus 和 blue 事件也是无法冒泡,无法取消的。我们为如下文档添加 focus 事件。
<
点击 input,显然,由于 focus 事件没有冒泡。控制台显示 input focus。
input focus
变更参数 useCapture 为 true,点击 input,此时控制台显示 container 和 input 的回调方法 。
document
提到 blur/focus,就会想到和它们很像的两个事件,那就是 focusout/focusin。它们和前者的主要区别就是 focusout/focusin 事件会冒泡。如果同时存在的话,focus 先于 focusin。blur 先于 focusout。
比较可惜的是,focusout/focusin 存在兼容性问题,例如 fireFox 低版本不支持这两个事件。当然,如果我们使用高版本浏览器的话,完全可以替代 blur/focus。
focus/blur 做事件委托的时候需要注意,在捕获阶段进行监听。否则会导致事件失效。
Media 事件
由媒介(比如视频、图像和音频)触发的事件,都不冒泡。以下列出部分事件。
...
onpause 当媒介被用户或程序暂停时运行的脚本
onplay 当媒介已就绪可以开始播放时运行的脚本
onplaying 当媒介已开始播放时运行的脚本
onsuspend 在媒介数据完全加载之前不论何种原因终止取回媒介数据时运行
...
假设我们有如下 audio 标签。
<
我们需要监听音频开始播放的事件。由于所有的由媒体触发的事件都不冒泡,所以我们只能在捕获阶段进行事件委托。
document
这样在上级元素中就能知道 audio 开始播放了。
container play
audio play
mouseleave & mouseenter
mouseleave 和 mouseenter 事件同样不会冒泡,当这种不冒泡的特性发生在鼠标事件的时候,显得额外的符合直觉。我们看下例子。
<
形状大概就是如下这个样子,从外到内越来越小。
_
我们绑定 mouseleave 事件,然后 我鼠标从 inner 滑动到 outer。
document
结果只触发了 inner mouseleave 事件,是不是很符合直觉。
与 mouseleave/mouseenter 事件非常相似的事件是 mouseout/mouseover,它们的区别就是 mouseout/mouseover 会触发冒泡,还是这个例子,我们将 mouseleave 换成 mouseout。
document
然后 鼠标从 inner 滑动到 outer。结果三个事件都触发了。
inner
在实际写代码的过程中,我们使用 mouseout 和 mouseover 会经常导致一些很奇怪的问题,遇到的时候,希望你能想起来,是事件冒泡搞得鬼。
JS 事件体系的原理比较容易理解,但是细节会比较繁琐一些,比如本文中罗列的这些事件,它们在做事件委托的时候就是坑,一不注意就会导致事件委托失效。
React 的事件体系就是基于事件委托建立的,其中有很大的部分都是处理不同事件之间的差异性的,对于这些不会冒泡的事件,React 也进行了处理。如果想了解 React 事件体系相关内容,请阅读我的 React 源码系列中的事件章节,不需要看 React 其余章节也能很容易的看懂。
如果您觉得有所收获,麻烦点个赞吧!