层叠上下文、z-index是面试中比较难的知识点,许多开发者都没有理解这些概念,导致项目中出现诡异的现象无法解决。
在翻阅了一些国人大佬的博客后,发现十分隐晦难懂,很啰嗦又抓不到重点。后翻阅老外博客、W3C标准,渐渐领悟了其中思想。本文不打算写成一个大而全的文章,无意取代其它资料,只是希望读者能理解最关键最核心的思想,之后再阅读其它资料便如鱼得水。
本文演示用的HTML
演示用的HTML如下:
<div class="a">
我是A
<div class="c">C是A的子元素</div>
</div>
<div class="b">
我是B
</div>
.a {
height: 150px;
width: 150px;
}
.b {
height: 80px;
width: 200px;
}
.c {
height: 100px;
width: 100px;
}

两个大家已经掌握的“常识”
现在我们让它们层叠在一起。按照规则,后出现的元素会覆盖在先出现的元素之上。所以元素按照A、C、B的顺序进行层叠。最终B覆盖在A和C之上。
#demo2 .a {
position: relative;
}
#demo2 .b {
position: absolute;
top: 60px;
}
#demo2 .c {
position: absolute;
}

接下来我们通过z-index让C在B的前面,A保持不动,所以效果是“B插在了AC之间”。
#demo3 .a {
position: relative;
}
#demo3 .b {
position: absolute;
top: 60px;
}
#demo3 .c {
position: absolute;
z-index: 1;
}

很多人以为z-index表示元素离屏幕的距离吧,以为z-index大的元素必定会覆盖小的元素吧?其实不是,这个误区是很多BUG的来源。
层叠上下文的原子性
现在,我们让A元素形成一个层叠上下文,方法有很多一般博客都有提到。这里我们就随便用个设置不透明度的方法:opacity: .9;。这时候z-index竟然就失效了,C跑到B的下面了,为何我们只是加了个透明度,布局就变了呢?
#demo4 .a {
position: relative;
opacity: .9;
}
#demo4 .b {
position: absolute;
top: 60px;
}
#demo4 .c {
position: absolute;
z-index: 1;
}

这是因为层叠上下文内具有原子性(atomic),其它元素无法“插入到AC之间”。 虽然C有z-index,但A和C是一个原子性整体,A在B下面,于是C也在B下面了(A是C的爸爸啊)。
「原子性」是很关键的概念,在W3C的原文是这样说的: 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.
「原子性」机制是为了简化浏览器实现,浏览器为不同层叠上下文分别渲染,最后把他们拼接起来。 拼接的时候浏览器只粗暴地考虑上下文之间整体叠放顺序,不再考虑不同上下文内部元素的顺序。 若没有「原子性」,浏览器将难以实现。
层叠顺序
在同一个层叠上下文中,子元素之间是按照什么顺序叠放的呢? 这点很多博客都总结了,我从这篇博客拷贝了下面这张图。类似的图还有很多,比如张鑫旭的博客也有一张差不多的图。
但是这些图总结的不全!请大家去翻W3C文档,下文我会再说到这个问题。

也就是说inline元素会显示在block元素的前面,我们把A改成inline,结果A压着B,效果和预期一致。
#demo5 .a {
display: inline;
}
#demo5 .b {
margin-top: -10px;
margin-left: 10px;
}
#demo5 .c {
position: absolute;
}

把A改回block,结果B压着A,效果和预期一致。
#demo6 .a {
display: block;
height: 25px;
}
#demo6 .b {
margin-top: -10px;
margin-left: 10px;
}
#demo6 .c {
position: absolute;
}

但是凭什么C在B的上面呢?这是那张图没有提到的,因为那张图总结的不全,遗憾的是国内多数开发者没有追根溯源的精神,导致出了问题毫无头绪。W3C原文如下:

可以看到W3C说的更全面,元素是否定位也会影响层叠结果。下面我们把C的position注释掉,然后B就压着C了,和W3C描述的一致。
#demo7 .a {
display: block;
height: 25px;
}
#demo7 .b {
margin-top: -10px;
margin-left: 10px;
}
#demo7 .c {
/* position: absolute; */
}

最后的思考练习
最后给大家的思考题,你知道为啥A、C都被B压着了,可是里面的文字还能显示出来吗?因为B是非定位元素,然而文字是inline,inline是在非定位元素前面的。
那么怎么让A、C的文字一起被B压着呢?方法有很多,大家思考,我这里随便给2个方法。
#demo8 .a {
display: block;
height: 25px;
}
#demo8 .b {
margin-top: -10px;
margin-left: 10px;
/* 方法一 */ /* position: relative; */
/* 方法二 */ transform: scale(1);
/* 其它方法自己多思考 */
}
#demo8 .c {
}
