在 Web 开发中,理解 HTML 节点的绘制顺序对于理解层级问题至关重要。本文基于 Appendix E. Elaborate description of Stacking Contexts,深入解析 HTML 节点的绘制顺序机制。
规范概述
根据 W3C 规范,在一个 Stacking Context 中,子节点的绘制遵循特定的顺序规则。规范中定义了以下关键步骤:
- Stacking contexts formed by positioned descendants with negative z-indices (excluding 0) in z-index order (most negative first) then tree order.
- For all its in-flow, non-positioned, block-level descendants in tree order
- All non-positioned floating descendants, in tree order. For each one of these, treat the element as if it created a new stacking context, but any positioned descendants and descendants which actually create a new stacking context should be considered part of the parent stacking context, not this new one.
- Otherwise: first for the element, then for all its in-flow, non-positioned, block-level descendants in tree order
- All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order. For those with 'z-index: auto', treat the element as if it created a new stacking context, but any positioned descendants and descendants which actually create a new stacking context should be considered part of the parent stacking context, not this new one. For those with 'z-index: 0', treat the stacking context generated atomically.
- Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index order (smallest first) then tree order.
核心概念
要深入理解绘制顺序规则,我们需要掌握以下核心概念:
- Stacking Context(Stacking Context):决定元素在视觉上的层叠顺序,是理解绘制顺序的基础
- position(定位):指定元素的定位方式,包括 static、relative、absolute、fixed、sticky
- z-index(层叠级别):设置定位元素(及其后代)和 flex/grid 项目在当前Stacking Context中的层级
- float(浮动):使元素脱离正常文档流,向左或向右浮动
基于这些标准,我们可以将元素进行如下分类:
分类 | CSS 属性 | 说明 |
---|---|---|
positioned | position: relative/sticky/absolute/fixed |
已定位元素 |
absolutely-positioned | position: absolute/fixed |
绝对定位元素 |
non-positioned | position: static |
非定位元素 |
non-floating | float: none |
非浮动元素 |
in-flow | float: none; position: static/relative/sticky |
流内元素 |
in-flow, non-positioned | float: none; position: static |
流内非定位元素 |
non-positioned floating | position: static; float: XXX |
非定位浮动元素 |
渲染顺序结构 的设计
绘制步骤解析
通过分析规范中的步骤,我们可以发现,在一个 Stacking Context 中,包含了下面三种类型的后代
- Step389:构成 Stacking Context 的元素,其中步骤 8 还包括定位元素
- Step5:浮动元素
- Step47:块级和内联元素
特殊的 Pseudo Stacking Context(PSC)
在 Step5 和 Step8 中,规范提到了一种构成特殊 Stacking Context 的元素:element as if it created a new stacking context, but any positioned descendants and descendants which actually create a new stacking context should be considered part of the parent stacking context。
为了与构成真实的 Stacking Context 的元素相区分,我们引入了 SC 和 PSC 的概念
- SC(Stacking Context):真实的Stacking Context
- PSC(Pseudo Stacking Context):上面提到的特殊的 Stacking Context。在构成 PSC 元素的后代中,构成 SC 和 positioned 的后代(Step389)会参与上一级的Stacking Context,其他后代才会在 PSC 内部排列。
渲染顺序结构
基于以上分析,我们只需要将构成 SC 和 PSC 的元素表达出来,即可表示完整的渲染顺序。这两种元素,我们用 PSCObject 和 SCObject 来表示。代码如下所示。其中,在 PSCObject 中包含了 三种类型的后代;在 SCObject 中包含了 六种类型的后代。
实际的渲染中,只需要按照 PSCObject
和 SCObject
的嵌套结构的先序顺序遍历,就是最终的渲染顺序。
需要注意的是,step7 中可能存在 textNode。这里为了简化模型,只考虑了 RenderableElement
ts
interface PSCObject {
element: RenderableElement, // 当前元素
step5: PSCObject[], // 浮动后代元素
step4: RenderableElement[], // 块级后代元素
step7: RenderableElement[], // 内联后代元素(注:实际渲染中可能包含文本节点)
}
interface SCObject {
element: RenderableElement, // 当前元素
step3: SCObject[], // 负 z-index 的Stacking Context
step8: (SCObject | PSCObject)[], // z-index 为 auto 或 0 的Stacking Context/伪Stacking Context
step9: SCObject[], // 正 z-index 的Stacking Context
step5: PSCObject[], // 浮动后代元素
step4: RenderableElement[], // 块级后代元素
step7: RenderableElement[], // 内联后代元素(注:实际渲染中可能包含文本节点)
}
判断 RenderableElement 属于哪个步骤
在实际应用中,某些元素可能同时满足多个步骤的条件。例如,一个浮动元素可能同时是块级元素。
遇到这种情况,需要根据步骤的优先级来确定元素属于哪个步骤:
优先级顺序: Step389
> Step5
> Step4/7
需要注意的是,在 Step8 中存在构成 SC 和 PSC 的元素。此时我们用 Step8_SC 和 Step8_PSC 表示。Step8_SC
的优先级高于 Step8_PSC
。
以下代码用于判断元素属于哪个步骤:
ts
// 绘制顺序步骤枚举
enum PaintingOrderStepForSC {
// 创建真实Stacking Context的元素
Step3, // 负 z-index 的Stacking Context
Step8_SC, // z-index 为 0 的Stacking Context
Step9, // 正 z-index 的Stacking Context
// 创建伪Stacking Context的元素
Step5, // 浮动元素
Step8_PSC, // 定位元素(z-index 为 auto)
// 普通元素
Step4, // 块级元素
Step7, // 内联元素
}
// 判断元素属于哪个绘制步骤
function getPaintingOrderStep(element: RenderableElement): PaintingOrderStepForSC {
// 优先检查是否创建Stacking Context
if (shouldBuildSC(element)) {
const zIndex = zIndexOrZero(element)
return zIndex < 0 ? PaintingOrderStepForSC.Step3
: zIndex === 0 ? PaintingOrderStepForSC.Step8_SC
: PaintingOrderStepForSC.Step9
}
// 检查是否为定位元素(z-index 为 auto)
if (isAbsoluteOrRelativeWithAutoZIndex(element)) {
return PaintingOrderStepForSC.Step8_PSC
}
// 检查是否为浮动元素
if (isStaticAndFloat(element)) {
return PaintingOrderStepForSC.Step5
}
// 根据显示类型判断
return isInlineElement(element) ? PaintingOrderStepForSC.Step7 : PaintingOrderStepForSC.Step4
}
构建渲染顺序结构
以下代码展示了如何构建 PSCObject
和 SCObject
结构。入口函数是 buildForDocumentElement
,它从文档根元素开始,递归收集完整的渲染顺序结构。
ts
// 构建文档根元素的Stacking Context结构
export function buildForDocumentElement(element: RenderableElement): SCObject {
assert(shouldBuildSC(element))
const result = toNullSCObject(element)
collectByTreeOrder(element, result, null, true)
return result
}
// 对象类型联合
type Object = SCObject | PSCObject | RenderableElement
// 按文档树顺序递归收集元素
function collectByTreeOrder(
element: RenderableElement,
scObject: SCObject,
pscObject: PSCObject | null,
inSC: boolean
) {
assert(inSC === (pscObject === null))
for (const child of element.children) {
// 过滤掉不可渲染的元素
if (!isRenderableElement(child)) {
continue;
}
const childStep = getPaintingOrderStep(child)
let childObject: Object = toObject(child, childStep)
// 根据当前上下文类型收集元素
if (inSC) {
collectForSC(scObject, childObject, childStep)
} else {
collectForPSC(scObject, pscObject!, childObject, childStep)
}
// 递归处理子元素
if (isSC(childStep)) {
collectByTreeOrder(child, childObject as SCObject, null, true)
} else if (isPSC(childStep)) {
collectByTreeOrder(child, scObject, childObject as PSCObject, false)
} else {
collectByTreeOrder(child, scObject, pscObject, inSC)
}
}
// 在Stacking Context中按 z-index 排序
if (inSC) {
scObject.step3.sort((a: SCObject, b: SCObject) => zIndexOrZero(a.element) - zIndexOrZero(b.element))
scObject.step9.sort((a: SCObject, b: SCObject) => zIndexOrZero(a.element) - zIndexOrZero(b.element))
}
}
// 根据步骤类型创建对应的对象
function toObject(element: RenderableElement, childStep: PaintingOrderStepForSC) {
if (isSC(childStep)) {
return toNullSCObject(element)
} else if (isPSC(childStep)) {
return toNullPSCObject(element)
}
return element
}
// 在Stacking Context中收集元素
function collectForSC(object: SCObject, childObject: Object, childStep: PaintingOrderStepForSC) {
switch(childStep) {
case PaintingOrderStepForSC.Step3:
object.step3.push(childObject as SCObject)
break
case PaintingOrderStepForSC.Step8_SC:
case PaintingOrderStepForSC.Step8_PSC:
object.step8.push(childObject as PSCObject)
break
case PaintingOrderStepForSC.Step9:
object.step9.push(childObject as SCObject)
break
case PaintingOrderStepForSC.Step5:
object.step5.push(childObject as PSCObject)
break
case PaintingOrderStepForSC.Step4:
object.step4.push(childObject as RenderableElement)
break
case PaintingOrderStepForSC.Step7:
object.step7.push(childObject as RenderableElement)
break
}
}
// 在伪Stacking Context中收集元素
function collectForPSC(scObject: SCObject, pscObject: PSCObject, childObject: Object, childStep: PaintingOrderStepForSC) {
switch(childStep) {
case PaintingOrderStepForSC.Step3:
scObject.step3.push(childObject as SCObject)
break
case PaintingOrderStepForSC.Step8_SC:
case PaintingOrderStepForSC.Step8_PSC:
scObject.step8.push(childObject as PSCObject)
break
case PaintingOrderStepForSC.Step9:
scObject.step9.push(childObject as SCObject)
break
case PaintingOrderStepForSC.Step5:
pscObject.step5.push(childObject as PSCObject)
break
case PaintingOrderStepForSC.Step4:
pscObject.step4.push(childObject as RenderableElement)
break
case PaintingOrderStepForSC.Step7:
pscObject.step7.push(childObject as RenderableElement)
break
}
}
实际案例
以下是一些实际案例,帮助理解不同情况下的绘制顺序:
案例 1:内联块级元素的绘制顺序
这个案例展示了内联块级元素和文本节点的绘制顺序。
html
<!-- 绘制顺序: Blue -> Red -> ABC -> 嘻嘻 -->
<style>
.box {
width: 100px;
height: 100px;
}
.inline-block {
display: inline-block;
}
.red {
background-color: red;
}
.blue {
background-color: blue;
margin-top: -40px;
}
.green {
background-color: green;
margin-left: 20px;
}
</style>
<div class="container">
<div class="box red inline-block">ABC
<div class="green box">123</div>
</div>
<div class="box blue">嘻嘻嘻</div>
</div>
案例 2:内联元素创建Stacking Context
这个案例展示了内联元素如何创建Stacking Context,属于步骤 8_SC。
html
<style>
.box {
width: 100px;
height: 100px;
}
#inline_with_SC {
background: rgba(255, 0, 0);
}
#positioned {
margin-bottom: -50px;
background: rgba(0, 255, 0);
}
</style>
<div>
<div id="positioned" style="position: relative;" class="box"></div>
<div id="inline_with_SC" style="display: inline-block;opacity: 0.8;" class="box"></div>
</div>
案例 3:CSS 属性优先级冲突
这个案例展示了当元素同时满足多个条件时的优先级规则:SC > position > float > block/inline
子案例 3.1: 非定位元素创建Stacking Context,属于步骤 8
html
<style>
.box {
width: 100px;
height: 100px;
}
</style>
<div id="container">
<div id="E3" class="box" style="position: absolute; z-index: -10; background-color: rgb(255, 0, 0); inset: 0px 50px;">E3</div>
<div id="E8" class="box" style="position: absolute;background-color: rgb(0, 255, 0); inset: 50px 0px;">E8</div>
<div id="E9" class="box" style="position: absolute; z-index: 10; background-color: rgb(0, 0, 255); inset: 75px 75px;">E9</div>
<div id="E8_opacity" class="box" style="background-color: rgb(0, 255, 255); opacity: 0.9;"></div>
</div>
子案例 3.2: 浮动元素创建Stacking Context
当浮动元素创建Stacking Context后,按照Stacking Context的顺序绘制。
html
<style>
.box {
width: 100px;
height: 100px;
}
.red {
background-color: red;
}
.blue {
background-color: blue;
inset: 50px;
}
</style>
<div class="container">
<div id="E8" class="blue box" style="position: absolute;"></div>
<div id="float_SC" class="red box" style="float: left; opacity: .9;"></div>
</div>
案例 4:Flex/Grid 项目受 z-index 影响
这个案例展示了 Flex 和 Grid 布局中的项目如何受到 z-index 的影响。
html
<style>
.box {
width: 100px;
height: 100px;
}
.red {
background-color: red;
}
.blue {
background-color: blue;
}
.green {
background-color: green;
inset: 30px;
}
</style>
<div id="container" style="display: flex;">
<div id="i1" class="box red"></div>
<div id="i2" class="box blue" style="margin-left: -50px; z-index: -10;"></div>
</div>
<div id="E3" class="box green" style="position: absolute; z-index: -5;"></div>
案例 5:完整Stacking Context中的绘制顺序
这个案例展示了在一个完整的Stacking Context中,所有类型元素的绘制顺序。
html
<style>
.box {
width: 100px;
height: 100px;
}
</style>
<div>
<div id="E3" class="box" style="position: absolute; z-index: -10; background-color: rgb(255, 0, 0); inset: 0px 50px;">E3</div>
<div id="E8_SC" class="box" style="position: absolute; z-index: 0; background-color: rgb(0, 255, 0); inset: 50px 0px;">E8_SC</div>
<div id="E9" class="box" style="position: absolute; z-index: 10; background-color: rgb(0, 0, 255); inset: 75px 75px;">E9</div>
<span id="E7" class="box" style="background-color: rgb(0, 180, 255);">E7</span>
<div id="E5" class="box" style="float: left; background-color: rgb(255, 0, 255)">E5</div>
<div id="E4" class="box" style="background-color: rgb(0, 128, 255);">E4</div>
</div>
案例 6:伪Stacking Context内的绘制顺序
这个案例展示了在伪Stacking Context(PSC)内部的绘制顺序。
html
<style>
.box {
width: 100px;
height: 100px;
}
#red {
background-color: red;
}
#blue {
background-color: blue;
margin-top: 50px;
}
</style>
<div id="red" style="position: absolute;" class="box">
<div id="blue" style="position: relative; z-index: -10;" class="box"></div>
</div>