HTML 节点绘制顺序详解:深入理解 Stacking Context

在 Web 开发中,理解 HTML 节点的绘制顺序对于理解层级问题至关重要。本文基于 Appendix E. Elaborate description of Stacking Contexts,深入解析 HTML 节点的绘制顺序机制。

规范概述

根据 W3C 规范,在一个 Stacking Context 中,子节点的绘制遵循特定的顺序规则。规范中定义了以下关键步骤:

  1. Stacking contexts formed by positioned descendants with negative z-indices (excluding 0) in z-index order (most negative first) then tree order.
  2. For all its in-flow, non-positioned, block-level descendants in tree order
  3. 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.
  4. Otherwise: first for the element, then for all its in-flow, non-positioned, block-level descendants in tree order
  5. 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.
  6. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index order (smallest first) then tree order.

核心概念

要深入理解绘制顺序规则,我们需要掌握以下核心概念:

基于这些标准,我们可以将元素进行如下分类:

分类 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 中包含了 六种类型的后代。

实际的渲染中,只需要按照 PSCObjectSCObject 的嵌套结构的先序顺序遍历,就是最终的渲染顺序。

需要注意的是,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
}

构建渲染顺序结构

以下代码展示了如何构建 PSCObjectSCObject 结构。入口函数是 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>
相关推荐
枕梦12614 小时前
Elpis:企业级配置化框架的设计与实践
前端
中微子14 小时前
Vue 2 与 Vue 3 组件写法对比
前端·javascript·vue.js
Nayana14 小时前
Element-Plus源码分析--button组件
前端·前端框架
中微子14 小时前
Vue 3 JavaScript 最佳实践指南
前端·javascript·vue.js
nightunderblackcat14 小时前
四大名著智能可视化推演平台
前端·网络·爬虫·python·状态模式
Mintopia14 小时前
Next.js 与 Serverless 架构思维:无状态的优雅与冷启动的温柔
前端·后端·全栈
小白而已14 小时前
事件分发机制
前端
蝙蝠编人生14 小时前
TailwindCSS vs UnoCSS 性能深度对决:究竟快多少
前端
ZXH012214 小时前
浏览器兼容性问题处理
前端