25-11-11-z-index与Vue3的静态提升


z-index 和 CSS 堆叠上下文

  • 浏览器中的默认堆叠情况
  • CSS 堆叠上下文
  • z-index 的原理

一、浏览器中的默认堆叠情况(无堆叠上下文干扰时)

在没有手动堆叠上下文(Stacking Context)的情况下,浏览器对元素的默认堆叠规则完全依赖于元素在文档流中的位置元素类型,核心规则如下:

  1. 文档流顺序(DOM 顺序)优先
    对于非定位元素position: static),后出现在 HTML 中的元素会覆盖先出现的元素(“后来居上”)。
    例:

    <div class="box1">先出现</div>
    <div class="box2">后出现</div>

    结果:box2 会覆盖 box1 重叠的部分。

  2. 定位元素(非 static)优先于非定位元素
    当元素设置 position: relative/absolute/fixed/sticky 时,会脱离文档流(或部分脱离),其堆叠层级高于所有非定位元素,无论 DOM 顺序如何。
    例:

    <div class="box1" style="position: relative;">定位元素(先出现)</div>
    <div class="box2">非定位元素(后出现)</div>

    结果:box1 覆盖 box2(定位元素层级更高)。

  3. 定位元素之间的默认顺序
    若多个元素都是定位元素,且未设置 z-index(或 z-index: auto),则仍遵循“后来居上”的 DOM 顺序。

二、CSS 堆叠上下文(Stacking Context)

堆叠上下文是浏览器渲染时创建的独立堆叠区域,区域内的元素堆叠顺序仅影响内部,无法直接穿透到外部。理解堆叠上下文是掌握 z-index 的核心。

1. 堆叠上下文的创建条件

一个元素会创建堆叠上下文的常见场景:

  • 根元素<html> 标签,默认创建“根堆叠上下文”);
  • 定位元素positionrelative/absolute/fixed/sticky)且 z-index 不为 auto(包括数值 0 或正负整数);
  • flex/grid 子元素z-index 不为 auto(即使不是定位元素);
  • CSS3 特殊属性opacity < 1transformnonefilternoneperspectivenoneisolation: isolate 等(无需定位也能创建)。

2. 堆叠上下文的特性

  • 独立性:每个堆叠上下文都是“封闭”的,内部元素的堆叠顺序不会影响外部,外部元素也无法直接影响内部(除非通过父级堆叠上下文的层级)。
  • 层级继承:子元素的堆叠层级受限于父级堆叠上下文的层级。例如,若父级堆叠上下文在底层,即使子元素 z-index: 9999,也无法覆盖父级上层的其他堆叠上下文。

三、z-index 的原理

z-index 用于控制同一堆叠上下文内元素的堆叠顺序,其工作机制依赖于堆叠上下文,核心原理如下:

1. z-index 的生效条件

仅对以下元素有效:

  • 定位元素(position: relative/absolute/fixed/sticky);
  • flex/grid 子元素(即使 position: static)。
    注意:非定位且非 flex/grid 子元素的 z-index 完全无效。

    “非定位元素”指的是未通过 position 属性进行定位设置,或明确设置 position: static 的元素(staticposition 的默认值)。这类元素遵循正常的文档流(normal flow),其位置由 HTML 结构和默认布局规则(如块级元素独占一行、行内元素并排)决定,无法通过 topbottomleftright 调整位置,也不会脱离文档流。

2. z-index 的值与含义

  • auto:默认值。元素不会创建新的堆叠上下文(除非是 flex/grid 子元素),其堆叠顺序由 DOM 顺序和定位状态决定。
  • 数值(包括正负整数和 0):
    • 定位元素设置数值 z-index 时,会创建新的堆叠上下文;
    • 数值越大,在同一堆叠上下文内的层级越高(但受限于父级堆叠上下文的层级)。

3. 同一堆叠上下文内的堆叠顺序(从后到前)

在一个堆叠上下文内部,元素的堆叠层级按以下顺序排列(优先级从低到高):

  1. 堆叠上下文的背景和边框(最底层);
  2. z-index负数的定位元素(及其创建的子堆叠上下文);
  3. 非定位元素(按 DOM 顺序排列);
  4. z-indexauto0 的元素(包括未创建堆叠上下文的定位元素、flex/grid 子元素等);
  5. z-index正数的定位元素(及其创建的子堆叠上下文)。

z-index: auto:默认不创建堆叠上下文(特殊情况除外)

  • 普通元素(非 flex/grid 子元素):
    当元素设置 z-index: auto 时(这是默认值,可省略),即使它是定位元素(position: relative/absolute 等),也不会创建新的堆叠上下文
    此时,该元素的堆叠顺序由以下规则决定:
    • 若为定位元素:层级高于非定位元素,与其他定位元素的顺序遵循“后来居上”(DOM 顺序)。
    • 其内部子元素的堆叠顺序可以“穿透”它,直接与外部元素比较(因为它没有创建独立的堆叠区域)。
  • 特殊情况:flex/grid 子元素
    若元素是 flex 容器(display: flex)或 grid 容器(display: grid)的直接子元素,即使设置 z-index: auto,也会创建新的堆叠上下文(这是 flex/grid 布局的特殊规则)。

4. 跨堆叠上下文的层级比较

若两个元素属于不同的堆叠上下文,则父级堆叠上下文的层级决定最终顺序,子元素的 z-index 仅在自身堆叠上下文内有效。
例:

<!-- 父元素 A:创建堆叠上下文,z-index: 1 -->
<div class="A" style="position: relative; z-index: 1;">
<div class="A-child" style="position: absolute; z-index: 999;">A 的子元素</div>
</div>

<!-- 父元素 B:创建堆叠上下文,z-index: 2 -->
<div class="B" style="position: relative; z-index: 2;">
<div class="B-child" style="position: absolute; z-index: 1;">B 的子元素</div>
</div>

结果:B-child 覆盖 A-child(因为 B 的堆叠上下文层级高于 A,子元素的 z-index 不影响父级比较)。

总结

  1. 无堆叠上下文时,元素堆叠依赖 DOM 顺序和定位状态;
  2. 堆叠上下文是独立的堆叠区域,由特定 CSS 属性触发,子元素层级受限于父级;
  3. z-index 仅在同一堆叠上下文内有效,用于调整元素顺序,其生效依赖定位或 flex/grid 子元素特性。

理解堆叠上下文的创建条件和层级规则,是避免 z-index 陷阱的关键。

VUE3 的静态提升

在《Vue.js 设计与实现》一书中,“静态提升”属于第 17 章编译优化的内容。
下面展示一下小标题,展示一下书籍的展示思路:


17.1 动态节点收集与补丁标志
17.1.1 传统 Diff 算法的问题

  • (静态节点不会被更新,但在 Diff 算法中仍然会进行比对;传统 Diif 算法在由渲染器执行,无法利用编译时提取到的任何关键信息,导致无法做相关优化。Vue.js3 则将编译时获得的关键信息附着到“虚拟 DOM”上,通过“虚拟 DOM”传递给渲染器)

17.1.2 Block 与 PatchFlags

  • (传统虚拟 DOM 中无任何标志能够体现出节点的动态性,而 Vue.js3 则会在编译器中将该信息“附着”到虚拟 DOM 上,如{tag: 'p', children: ctx.bar, patchFlag:1}里的patchFlag,不同数字值由不同含义)。有了这些信息,就可以在虚拟节点的创建阶段,把它的动态子节点提取出来,并存储到该虚拟节点的 dynamicChildren 数组内。我们把带有该属性的虚拟节点称为“块”,即Block。一个 Block 不仅能够收集它的直接动态子节点,还能够收集所有动态子代节点。

  • 所有模板的根节点都会时一个 Block 节点;任何带有 v-for、v-if/v-else-if/velse 等指令的节点都需要作为 Block 节点。( block 本质上也是一个 vnode )

    17.1.3 收集动态节点

    • 前言:渲染函数代码会使用来创建虚拟 DOM 节点的辅助函数createVNode。createVNode 函数的返回值是一个虚拟 DOM 节点。在 createVNode 函数内部,通常还会对 props 和 children 做一些额外的处理工作。
    • 编译器优化提取的信息,体现在于创建虚拟 DOM 节点的辅助函数上。例如由模板<p class="bar">{{ text }}</p>,优化后就会生成带有补丁标志(patch flag)的渲染函数,如createVNode('p', { class: 'bar' }, text, PatchFlags.TEXT) // PatchFlags.TEXT 就是补丁标志
    • 接下来的问题就是如何收集动态节点。首先,渲染辅助函数是嵌套调用的,先执行内层函数,再执行外层函数。为了让外层 Block 节点收集内层动态节点,就需要一个栈结构的数据来临时存储内层的动态节点dynamicChildrenStack。在迭代收集完毕后,将currentDynamicChildren赋值给block.dynamicChildren

    17.1.4 渲染器的运行时支持

    • 有了动态节点集合 vnode.dynamicChildren,以及附着其上的补丁标志,我们就可以在渲染器中实现靶向更新。现在我们的节点更新函数patchElement可以直接对比动态节点,在其中调用patchBlockChildren(n1, n2)方法更新动态节点。这样就可以跳过所有静态节点。
    • 对于具体的节点,可以针对patch flag的值,针对性地完成靶向更新。如patch flag=1表示只有 class 会变更,只需要更新 class。

    17.2 Block 树
    除了组件模板的根节点,还有其它节点会作为 Block 角色

    17.2.1 带有 v-if 指令的节点
    dynamicChildren 数组中收集的动态节点是忽略虚拟 DOM 树层级的。结构化指令会导致更新前后模板的结构发生变化,即模板结构不稳定。因此要让带有 v-if/v-else-if/v-else 等结构化指令的节点也作为 Block 角色。

    17.2.2 带有 v-for 指令的节点

    01 const block = {
    02 tag: 'div',
    03 dynamicChildren: [
    04 // 这是一个 Block,它有 dynamicChildren
    05 { tag: Fragment, dynamicChildren: [/* v-for 的节点 */] }
    06 { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
    07 { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    08 ]
    09 }

17.2.3 Fragment 的稳定性
在上一节中,我们使用了一个 Fragment 来表达 v-for 循环产生的虚拟节点,并让其充当 Block 的角色来解决 v-for 指令导致的虚拟 DOM 树结构不稳定问题。但是,我们需要仔细研究这个 Fragment 节点本身。
Fragment 本身收集的动态节点仍然面临结构不稳定的情况。所谓结构不稳定,从结果上看,指的是更新前后一个 block 的 dynamicChildren 数组中收集的动态节点的数量或顺序不一致。这种不一致会导致我们无法直接进行靶向更新。但对于这种情况,没有更好的解决办法,智能回退到传统虚拟 DOM 的 Diff 手段。

17.3 静态提升
没静态提升时,静态的虚拟节点也会随着渲染函数重新渲染:

01 function render() {
02 return (openBlock(), createBlock('div', null, [
03 createVNode('p', null, 'static text'),
04 createVNode('p', null, ctx.title, 1 /* TEXT */)
05 ]))
06 }

这是没有必要的,而解决方案就是所谓的“静态提升”,即把纯静态的节点提升到渲染函数之外:

01 // 把静态节点提升到渲染函数之外
02 const hoist1 = createVNode('p', null, 'text')
03
04 function render() {
05 return (openBlock(), createBlock('div', null, [
06 hoist1, // 静态节点引用
07 createVNode('p', null, ctx.title, 1 /* TEXT */)
08 ]))
09 }

需要强调的是,静态节点是以树为单位的;
包含动态绑定的节点本身不会被提升,但是该动态节点上仍然可能存在纯静态的属性,最终生成渲染函数时,我们可以将纯静态的 props 提升到渲染函数之外。
17.4 预字符串化
基于静态提升,我们还可以进一步采用预字符串化的优化手段。预字符串化是基于静态提升的一种优化策略。如果静态提升的虚拟节点或虚拟节点树本身是静态的,就可以进行预字符串化,把这些静态节点序列化为字符串,并生成一个 Static 类型的 VNode。
优势:

- 大块的静态内容可以通过 innerHTML 进行设置,在性能上具有一定优势。
- 减少创建虚拟节点产生的性能开销。
- 减少内存占用

17.5 缓存内联事件处理函数 (略)
17.6 v-once
编译器遇到 v-once 指令时,会利用我们上一节介绍的 cache 数组来缓存渲染函数的全部或者部分执行结果;
从编译结果中可以看到,该 div 标签对应的虚拟节点被缓存到了 cache 数组中。既然虚拟节点已经被缓存了,那么后续更新导致渲染函数重新执行时,会优先读取缓存的内容,而不会重新创建虚拟节点。同时,由于虚拟节点被缓存,意味着更新前后的虚拟节点不会发生变化,因此也就不需要这些被缓存的虚拟节点参与 Diff 操作了。
v-once 指令通常用于不会发生改变的动态绑定中,例如绑定一个常量。


《Vue.js 设计预实现》已经把静态提升讲得很清除了。下面也展示 AI 对静态提升的讲述,或许未看过上面提到的这本书的读者更容易接受下面的表述。

在 Vue 3 中,静态提升(Static Hoisting) 是编译器层面的优化技术,用于提升模板中静态节点(不依赖任何动态数据、不会被更新的节点)的性能。

一、静态提升的核心原理

当 Vue 3 编译器解析模板时,会识别出完全静态的节点(如纯文本、无动态绑定的元素),并将其从渲染函数中“提升”出来,成为一个独立的、可复用的常量。

这样做的好处是:

  • 避免在每次组件重新渲染时(如响应式数据变化触发更新)重复创建这些静态节点的虚拟 DOM(VNode);
  • 减少渲染函数的执行时间和内存开销,提升性能。

二、示例对比

假设模板中有一段静态内容(无任何动态绑定):

<template>
<div class="static-container">
<h1>这是一个静态标题</h1>
<p>这是一段静态文本,不依赖任何响应式数据。</p>
<p>当前时间: {{ time }}</p>
<!-- 动态节点 -->
</div>
</template>

未启用静态提升时(模拟):

每次次组件渲染(如 time 变化时),渲染函数会重新创建所有节点的 VNode,包括静态的 <h1><p>

function render() {
return createVNode('div', { class: 'static-container' }, [
createVNode('h1', null, '这是一个静态标题'), // 重复创建静态节点
createVNode('p', null, '这是一段静态文本...'), // 重复创建静态节点
createVNode('p', null, `当前时间: ${time}`) // 动态节点
])
}

启用静态提升后(Vue 3 实际编译结果):

静态节点被提升为渲染函数外部的常量,仅在组件初始化时创建一次,后续渲染直接复用:

// 静态节点被提升为常量,只创建一次
const hoistedStaticNodes = [
createVNode('h1', null, '这是一个静态标题'),
createVNode('p', null, '这是一段静态文本...')
]

function render() {
return createVNode('div', { class: 'static-container' }, [
...hoistedStaticNodes, // 直接复用静态节点
createVNode('p', null, `当前时间: ${time}`) // 仅动态节点重新创建
])
}

三、静态提升技术的背景

Vue 3 引入静态提升的核心背景是提升大型应用的渲染性能,主要源于对 Vue 2 渲染机制的优化:

  1. Vue 2 的性能瓶颈
    在 Vue 2 中,无论节点是否静态,每次组件更新都会重新创建所有 VNode,然后通过 Diff 算法对比新旧 VNode 树。对于包含大量静态内容(如页面布局、固定文本)的组件,这种重复创建和对比会产生不必要的性能开销。

  2. 编译器与运行时的协同优化
    Vue 3 重构了编译器和响应式系统,强调“编译器主导的优化”。通过编译阶段识别静态节点并提升,减少运行时的 VNode 创建和 Diff 工作量,实现“按需更新”(只处理动态节点)。

  3. 针对静态内容占比高的场景
    在实际业务中,很多页面包含大量静态内容(如导航栏、页脚、说明文本),静态提升能显著减少这些内容在更新时的性能损耗,尤其在高频更新场景(如数据实时刷新)中效果明显。

总结

静态提升是 Vue 3 编译器对静态节点的优化,通过复用静态 VNode 减少渲染开销。其背景是解决 Vue 2 中静态内容重复创建的性能问题,通过编译器与运行时的协同,让 Vue 3 在处理大量静态内容时更高效。


文章作者: Qijia Huang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Qijia Huang !
  目录