z-index和event bubbling

前几天调查一个前端的bug,最终结果是和z-index及event bubbling有关,正好有段时间没写博客了,就此机会将问题的分析和解决过程记录下来,顺便回顾一下这两个知识点吧!
先贴参考资料:
[1] https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float
[3] http://www.quirksmode.org/js/events_order.html

1、bug的背景和现象
某设备的远程web访问页面,用户在载入页面或者进行操作后,会先出现如下图所示的”动作进行中”的画面——一个全屏的modal window覆盖在下方的画面上,中间的圆圈转啊转:
home_screen_processing

页面载入完成、或动作完成后,modal window消失,下方的页面显示出来:
home_screen

发现的问题是,当用户在转圈页面闲着无聊不停点击右上角的”Login”按钮时,有时候会发现浏览器报javascript错误,位置是在按钮的mouseup事件处理函数里,错误内容是访问了一个null变量,但此变量理论上应该在之前的mousedown事件里已经初始化了。而且此问题不是100%重现,有时候报错有时候却不报。

2、问题分析和解决
凭直觉认为是右上角那个button的mousedown事件没有被触发,变量没有初始化,因此后面的mouseup事件处理函数里,访问到了null变量。那么为什么现象不是100%重现呢?此时怀疑只有modal window尚在表示的时候按下鼠标左键,modal window消失后松开鼠标左键的时候,才会报错。因为这种情况下,mouseup事件有可能被modal window捕获到,并且没有传到button处,所以前者只触发了mousedown,后者只触发了mouseup。

为了验证这个猜想,特地给modal window和button的鼠标事件打了event.timeStamp的log,确定事件的发生次序和目标元素。结果果然不出所料:
event_timestamp
在js出错前,modal window只有mousedown,button(上图中的contextmenu)只有mouseup!
另外还发现在modal window表示期间按下鼠标,消失后松开鼠标,100%会报js错误,这也从侧面证明了猜想的正确。

知道问题就好解决了,只要在button的mouseup事件处理函数里,判一下那个变量是否为null,是的话赋个初值即可。

3.知其然,知其所以然
如果为了应付工作,做到上面已经足够了。但是作为一个有追求的程序员,这里肯定是要知道为什么button的mousedown事件没有被触发。因此,下面一起重温一下与此有关的z-index和event bubbling(事件冒泡)知识吧!

先轮到z-index出场。根据MDN上的资料[1],满足下述任一条件的DOM元素,都会在它们的所在位置形成一个”stacking context (层叠上下文)”:
○ html标签
○ position = absolute或relative,并自身z-index的值不为auto
○ 是display = flex或inline-flex父元素的子元素,且自身z-index值不为auto
○ 拥有opacity的style,且值小于1
○ 拥有transform的style,且值不为none
○ 拥有mix-blend-mode的style,且值不为normal
○ 拥有filter的style,且值不为none
○ 拥有perspective的style,且值不为none
○ 拥有isolation的style,且值为isolate
○ position = fixed
○ 拥有will-change的style,其值为上述任意属性名(style名)
○ 拥有-webkit-overflow-scrolling的style,且值为touch
简单地说,相同层级的stacking context,根据z-index的大小决定相互覆盖关系,如果都没有指定z-index,则按照它们在DOM中的出现顺序,后面的覆盖前面的。
值得注意的是,当前stacking context内的子元素的z-index值仅仅相对于当前层级有效,如果当前stacking context已经被其他stacking context覆盖,即使子元素的z-index再高,也不会超出当前stacking context的平面,因此依然是被覆盖的。
另外,float元素也会影响层级关系,有兴趣可以研究下参考文献[2]。

具体到本次问题,首先把画面的DOM结构表示出来:
home_page_hierarchy
上文中的modal window和button所对应的DOM元素,都已经用红线圈起来了。其中那个”display:none”请忽略,因为modal window显示中不好截图,只截了它被隐藏后的DOM结构。
下方红框中的modal window,自身position:fixed,因此满足条件,形成一层stacking context。而其父元素#processingModal是position:static,因此不形成stacking context。再往上是position:relative的body,但是因为body的style里没有z-index,所以也不形成stacking context。再往上就是html了,这个会形成一层stacking context。
另一方面,上方红框中的button元素虽然是postion:relative,但是也因为没有z-index属性,因此不形成stacking context。一路往上走,直到header这一层,才是position:absolute并且指定了z-index:3,因此形成了一层stacking context。

示意图:
stack_context_graph
如上图所述,这个页面的stacking context层级是这样的:html作为根元素,位于最下层,其上有两个平级的stacking context,即header和div.xux-modal。这时候z-index就起作用了,header的z-index为3,而modal window的则为99999996,那么后者就会覆盖在前者上面。当全屏modalwindow表示的时候,因为它在画面最上层铺满了,如果点击鼠标,则会触发它的mousedown事件。

剩下的问题就是,为什么下方的button没有接收到这个事件呢?即使被modal window盖住,点击的位置也还是button的正上方啊。
这里就牵扯到javascript事件冒泡的问题了[3]。其实在冒泡之前,会先经历event capturing过程,但是本问题中使用的是jquery的on()方法来注册事件处理函数,而jquery是忽略event capturing的,因此这里我们只讨论event bubbling。
接着上面的分析结果,用户按下鼠标的时候,mousedown事件在modal window上触发了。javascript事件冒泡的原理,简单来说就是事件会沿着DOM树一直往上爬,沿途的所有元素都会触发此事件。但是它不会拐弯或者往下走。

我们再看一下本问题的DOM截图,mousedown冒泡的路径是div.xux-modal -> #processingModal -> body -> html,不会中途拐个弯去header标签,更不会再从header往下走去button。因此,在modal window处触发的mousedown事件是不会传递到button去的。结合z-index,这就是问题的根本原因。

本文是悠然居(wordpress.youran.me)原创文章,如转载必须保留此告示。

本文为悠然居(https://wordpress.youran.me/)的原创文章,转载请注明出处!

Leave a Reply

Your email address will not be published. Required fields are marked *