下拉菜单「点击外面关闭」的终极解决方案

6,925 阅读3分钟

场景:react项目。

一般遇到这种问题网上的说法都是:

  1. 给点击开启下拉菜单的Dom元素方法中添加e.stopPropagation()阻止事件冒泡
  2. 再给document添加一个监听点击的事件:

    document.addEventListener('click', this.offDropMenu, false)

可是这种一般只能适用于普通的下拉导航菜单,点击菜单就切换页面然后菜单就消失的这种情景,比如下面这种:

但是遇到这种复杂的菜单,当用户点击到菜单内部时不想让菜单消失该怎么办?


网上千篇一律的帖子又会说了,在下拉菜单的最外层Div再添加一个addEventListener监听点击事件,事件内只写一个e.stopPropagation()。


确实,点击菜单内部是不跳转了,但是里面的事件也全被阻止掉了呀!

点击都没反应了……

难道要我给每个子元素都添加阻止冒泡?

我思索片刻🤔,想到了一个很原始很暴力的方法,就是根据鼠标点击的位置来避开菜单😂

(⚠️此代码可能会造成大神的不适~)

然后看着功能还凑合,也实现了业务需求,就那么放着了。

直到后来某一天,我发现了Dom元素有一个原生的方法contains()就是为了验证知道某个节点是不是另一个节点的后代,返回一个布尔值:

document.documentElement.contains(document.body)  // true

这简直就是神器呀!刚好适用于这个场景,管你冒不冒泡:


但是还遇到一个问题,那就是e.stopPropagation()无法阻止冒泡。


参考React的事件系统,如果出于某些原因想使用浏览器原生事件,可以使用 nativeEvent 属性获取。

e.nativeEvent.stopImmediatePropagation() 替换掉e.stopPropagation() 可以达到效果。

下面进入探索环节,经查阅资料,得出以下结论:
1. React为了提高效率,把事件都委托给了document,所以 e.stopPropagation() 并非是不能阻止冒泡,而是等他阻止冒泡的时侯,事件已经传递给document了,没东西可阻止了。可以通过在document.body上绑定 alert(3),直观的了解这一点,3 是优先于 1 弹出的。
2. e.stopPropagation()不行,浏览器支持一个好东西,e.stopImmediatePropagation() 他不光阻止冒泡,还能阻止在当前事件触发元素上,触发其它事件。这样即使你都绑定到document上也阻止不了我了吧。
3. 这样做还不行,React对原生事件封装,提供了很多好东西,但也省略了某些特性。e.stopImmediatePropagation() 就是被省略的部分,然而,他给了开口:e.nativeEvent ,从原生的事件对象里找到stopImmediatePropagation(),完活。

以上来自CNblog

stopImmediatePropagation和stopPropagation的区别

  • stopImmediatePropagation :
阻止事件流中当前节点的和所有后续节点的事件监听器的执行。即影响当前结点的事件监听器。
  • stopPropagation:
阻止事件流中当前节点的所有后续节点的事件监听器的执行。即不会影响当前节点(currentTarget)的任何事件监听。

举个🌰:给一个元素添加三个点击事件(注册顺序,就是代码的执行顺序)


  1. 如果是e.stopPropagation(),执行结果为依次弹出 1,2,2.1,3。因为不会影响到当前节点的事件
  2. 若是e.stopImmediatePropagation(),结果则为 1,2,2.1。


总结:只是一个很好用的contains()方法,记得最开始学习的时候是看过这个属性,归根到底还是自己基础没打牢!