永不消失的遮罩:鲜为人知的 Context 大坑

文章目录
  1. 1. 永不消失的遮罩
  2. 2. Stacking Context
    1. 2.1. Stack Level
    2. 2.2. Features
    3. 2.3. Establishes A Stacking Context
  3. 3. Analyze & Solve Problem
    1. 3.1. 问题描述
    2. 3.2. 科学解释
    3. 3.3. 问题联想
  4. 4. 参考文档

这是一个令人费解的遮罩问题,经过各种调试与查阅资料后,发现由 z-index 一路牵扯到 Stacking Context,浑水竟然如此之深…

永不消失的遮罩

众所周知,z-index这货能控制元素的层级,遵循近大远小、后来居上的规则,可以把元素举高高或者打入幕后。
比如在做各种浮层/层叠定位的时候,拿来用一用。

直到最近调试了一个诡异的弹层页面(示例),遮罩死皮赖脸的cover全场 (╯‵□′)╯︵┻━┻
调试过程略过,下面梳理一下涉及的知识点,从stacking context(层叠上下文)说起:

Stacking Context

The bottom of the stack is the furthest from the user.
The top of the stack is the nearest to the user.



| | | |
| | | | ⇦ ☻
| | | user
z-index: canvas -1 0 1 2


在HTML的世界里,除了x轴(水平)和y轴(竖直)的维度,还有z轴(垂直屏幕)的维度;

可以想象在z轴上存在很多个层,那么处于底部的元素距离用户最远,而在顶部的元素则距离用户最近,相对下层的元素,用户会先看到其上部的元素;

在这个维度中,通过对比z-index的值,来决定各个层最终如何展示在用户的视野中。而这种通过z-index对比层级关系并影响子元素渲染顺序的结构,我们称之为层叠上下文(Stacking Context)。


### Z-index

>

For a positioned box, the ‘z-index’ property specifies:


- The stack level of the box in the current stacking context.
- Whether the box establishes a stacking context.

从W3的文档里可以了解到,对于定位的盒模型,z-index声明:

  • 在当前的层叠上下文中,层叠的水平
  • 元素是否创建层叠上下文

那么再来看z-index的值,它存在两种方式:

<integer>

  • This integer is the stack level of the generated box in the current stacking context. The box also establishes a new stacking context.

auto

  • The stack level of the generated box in the current stacking context is 0. If the box has ‘position: fixed’ or if it is the root, it also establishes a new stacking context.

那么便存在数值auto两种类型的值,其中auto生效时,其在数值上与0相同。
数值则表明了当前元素位于当前层叠上下文中的stack level,翻译过来叫层叠水平

Stack Level

  • Boxes with greater stack levels are always formatted in front of boxes with lower stack levels.
  • Boxes with the same stack level in a stacking context are stacked back-to-front according to document tree order.

对于不同元素间的描述,这里抽出了两句有用的,总结起来就是:

  • 近大远小:元素层叠水平数值大的比小的更靠前(前者覆盖后者:100 > 1 > auto = 0 > -1)
  • 后来居上:元素层级一致、层叠水平数值一致时,靠后的元素覆盖前面的元素

那么对于在同一个层叠上下文内的各层,其back-to-front order按下面👇的描述进行展示:

Within each stacking context, the following layers are painted in back-to-front order:

  • the background and borders of the element forming the stacking context.
  • the child stacking contexts with negative stack levels (most negative first).
  • the in-flow, non-inline-level, non-positioned descendants.
  • the non-positioned floats.
  • the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
  • the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
  • the child stacking contexts with positive stack levels (least positive first).

看完上代码实测一下,用图形示意就是:

background/borders
z-index < 0
block
floats
inline
z-index: 0 === auto
z-index > 0







Back-to-front order

Features

  • Stacking contexts can contain further stacking contexts.
  • A stacking context is atomic from the point of view of its parent stacking context;
  • boxes in other stacking contexts may not come between any of its boxes.
  • Each box belongs to one stacking context.
  • Each positioned box in a given stacking context has an integer stack level, which is its position on the z-axis relative other stack levels within the same stacking context.

对于层叠上下文,特性总结起来就是:

  • 可以嵌套
  • 其层叠特性并不对内部元素产生影响
  • 每个层叠上下文相对于其他元素是完全独立的
  • 每个元素都将处于一个层叠上下文中
  • 子元素以其父元素(parent stacking context)为z-index相对基准点,拥有相对于同一层叠上下文内的层叠水平数值

Establishes A Stacking Context

  • The root element forms the root stacking context.
  • Other stacking contexts are generated by any positioned element (including relatively positioned elements) having a computed value of ‘z-index’ other than ‘auto’.

起初,层叠上下文以两种形式存在:

  • 根元素<html>会形成顶级的层叠上下文
  • 给一个已定位元素(positioned element)指定一个具体的值(auto除外)

Stacking contexts are not necessarily related to containing blocks.
In future levels of CSS, other properties may introduce stacking contexts.

而现在由于CSS3的出现,又多了一些由CSS属性直接导致的层叠上下文生成的方式:

  • opacity ≠ 1
  • filter ≠ none
  • isolation = isolate
  • transform ≠ none
  • mix-blend-mode ≠ normal
  • position = fixed(mobile webkit & chrome 22+)
  • z-index ≠ auto的flex项(父元素display:flex|inline-flex)
  • will-change = 上面任意属性名
  • -webkit-overflow-scrolling = touch & overflow ≠ (visible/hidden/unset)

这些规则会导致非常诡异的问题,比如本文遇到的那个坑,查看这个诡异的弹层页面(示例)

Analyze & Solve Problem

通过学习并理解以上的知识点,现在来解释一下诡异的问题到底是怎么出现的!
实际上debug的步骤,则是刚好相反的,先试验分析再找理论支持,并解释问题出现的原因

问题描述

1
2
3
4
<div id="app">
<div id="dialog" style="position: fixed; z-index: 101;">dialog</div>
</div>
<div id="mask" style="position: fixed; z-index: 100;">mask</div>

这个结构不是什么好例子🌰,但是恰好某个组件是这么实现的,所以为了排查问题,抽离了最核心的DEMO,就是以上这个DOM结构

假设现在#app不具备前文所述的任何一种产生层叠上下文的条件,那么此时#dialog#mask应当作为同级别层叠上下文来看待,并遵循近大远小的原则,dialog应当覆盖在mask之上。(😂确实我们想要这个效果)

但现在由于某种神奇的原因,在#app上添加了-webkit-overflow-scrolling: touch; overflow: auto属性,此时悲剧发生了:IOS手机打开,mask覆盖到了dialog之上(😱WAHT HAPPENED!?)

后来经过试验,#app元素若存在上述9种任意一种属性/组合,都会导致这个诡异的状态出现!!!😱AMAZING!!!

科学解释

用上面的原理解释一下,当#app元素存在上述9种任意一种属性/组合时,发生了什么:

  • #app会生成新的叠层上下文,此时其内部元素#dialog就变成其嵌套层叠上下文
  • #dialog即以#app的层叠上下文为基准,不再和#mask作同级对比
  • #app的z-index相当于auto,并在数值上与0相等

这意味着:

  • #dialog将在#app的层叠上下文内渲染
  • #mask将覆盖在#app之上,因为:#app:auto < #mask:100

最终,导致了:

  • #mask覆盖在#dialog之上. OH NO !😯

问题联想

恰好在这个例子中,我遇到了-webkit-overflow-scrolling: touch; overflow: auto这个组合导致的问题,所以曾经一度联想,是不是因为BFC导致的,并发现一篇很好的文章:

BFC元素特性表现原则就是,内部子元素再怎么翻江倒海,翻云覆雨都不会影响外部的元素

但是作为noZUOnoDIE星人,还是必须动手试一下的嘛。结果发现根本没有这回事,跟BFC一点关系都没有!!!BFC表示拒绝背锅~🙅🙅🙅
并且在严谨的控制变量法下发现:只有overflow ≠ (visible/hidden/unset)时,-webkit-overflow-scrolling: touch才会使当前元素生成叠层上下文

这就是为什么在ISO手机上死活关不掉这个遮罩层的原因所在了吧😑

参考文档