记一次事件捕获的妙用

JavaScript事件流有事件捕获, 处于目标及事件冒泡三个阶段。 在使用addEventListener注册事件的时候, 其第三个参数也可以选择将事件注册在哪个阶段。 平时我们事件大多都是注册在冒泡阶段的, 所以第三个参数一般都是传递的false, 由于很少使用到事件捕获, 所以久而久之也就忘了还有这一茬了。但是其实在某些特定的场景下, 事件捕获还是挺有用的。

在之前的业务中, 碰到了一个关于弹窗的关闭顺序的问题。

由于在系统中使用了popoverinside-modal, modal, dropdown等弹窗, 并且存在级联popover, 然后为了方便管理esc关闭弹窗的功能, 同事做了一个esc事件队列管理器, 其原理就是对document进行代理, 在按下esc的时候, 优先触发最后绑定的事件。

本来这个功能用的好好的, 但是后来发现了一个问题。我们系统中所使用的第三方的文件预览插件Magnific-Popup所附带的弹窗也是在document上绑定了esc退出功能的, 但是由于该插件并没有具体地集成到系统中, 所以他绑定的事件是没有加入到事件队列管理器里面的。这样造成的问题是, 在文件(inside-modal弹窗)中点击文件预览的时候, 打开了弹窗, 当按下esc的时候, 本来期望的只是关闭预览弹窗, 但是由于队列管理器对事件触发顺序进行了处理, 最后结果是先关闭了文件(inside-modal)弹窗, 然后才会关闭预览弹窗。毫无疑问, 这个问题的影响是比较严重的。

文件弹窗 :
inside-modal

文件预览弹窗:
文件预览弹窗

在这种情况下,直接阻止事件冒泡是没有效果的, 因为事件冒泡只是阻止事件的传递而已, 并且由于两者事件都是注册在document上的, 而这里涉及到的是事件触发顺序的问题(管理器里的事件优先触发)。

所以, 这个时候就可以用到事件捕获了。 我们知道, 事件捕获是从最外层向最里层传递的, 也就是从window上面开始传递, 所以这里可以对window绑定文件预览弹窗的esc事件:

1
2
3
4
5
6
7
8
9
10
11
12
window.addEventListener('keydown', _closeDialog, true); // 注意第三个参数是true

function _closeDialog (e) {
if(e && e.keyCode === 27) {
window.removeEventListener('keydown', _closeDialog, true);
mfp.close();
e.stopPropagation();
e.preventDefault();
} else if (!e) {
window.removeEventListener('keydown', _closeDialog, true);
}
};

在其事件处理程序中, 调用事件对象的stopPropagation()来阻止事件向下一层元素传递, 这样一来document上就不能接收到该事件了, 从而使得在文件预览弹窗里按下esc的时候, 事件队列处理器不能够接收到相应事件,保证了预期效果。

最后总结一下, 这里总共做了两个操作:

  • 将预览弹窗的keydown事件注册到window上面
  • 将该事件注册在事件捕获阶段

需要注意的是, 移除事件所用的参数只有和绑定事件的参数一致的时候才能够准确地移除事件哈。

当然,也许针对这个问题还会有更好的解决方案, 只是当时为了保证开发效率, 就直接采用能够想到的方案了。