首页
/ Svelte {if} 条件渲染块:语法详解、编译器管线与运行时分支机制

Svelte {if} 条件渲染块:语法详解、编译器管线与运行时分支机制

2026-09-05 09:13:20作者:乔或婵

本文围绕 Svelte 模板语法中的 {#if} 条件块展开:先完整梳理其三种基础语法形式与使用要点,再沿着 Svelte 5 编译器的真实代码路径,深入解析 if 块从解析(Parse)、分析(Analyze)到客户端/服务端转换(Transform)的完整管线,以及运行时 BranchManager 的分支挂载机制。读完本文,你不仅能正确使用 {#if}/{:else if}/{:else} 编写条件渲染逻辑,还能理解 else-if 链的编译期展平、水合锚点标记等底层原理,并能对照源码定位相关行为。

一、{#if} 的三种基础语法形式

官方文档(见 02-if.md)定义了 if 块的全部三种形态:

<!-- 形式 1:单一条件 -->
{#if expression}...{/if}

<!-- 形式 2:带 else if 与 else -->
{#if expression}...{:else if expression}...{:else}...{/if}

<!-- 形式 3:带 else -->
{#if expression}...{:else}...{/if}

条件渲染的内容用 if 块包裹,当表达式为真时渲染块内内容,否则移除(或不渲染)。最简形式:

{#if answer === 42}
	<p>what was the question?</p>
{/if}

通过 {:else if expression} 可以追加多个条件分支,最后可以(也可以不)以 {:else} 子句收尾。官方文档给出的经典示例:

{#if porridge.temperature > 100}
	<p>too hot!</p>
{:else if 80 > porridge.temperature}
	<p>too cold!</p>
{:else}
	<p>just right!</p>
{/if}

一个容易忽略的要点:if 块不必包裹整个元素,也可以包裹元素内部的文本。例如:

<p>当前状态:{#if online}在线{/else}离线{/if}</p>

块内可以放文本、元素、组件、嵌套块({#each}{#await}、另一个 {#if} 等),这与 Svelte AST 中 consequent/alternate 均为 Fragment(节点数组)的设计一致(见下文第二节)。

二、AST 表示:IfBlock 节点

编译器解析后的 if 块对应 AST.IfBlock 类型,定义于 template.d.ts

/** An `{#if ...}` block */
export interface IfBlock extends BaseNode {
	type: 'IfBlock';
	elseif: boolean;              // 是否由 {:else if} 产生
	test: Expression;             // 条件表达式
	consequent: Fragment;         // 为真时渲染的节点列表
	alternate: Fragment | null;   // else 分支(或嵌套的 else-if 块),无 else 时为 null
	metadata: {
		/** List of else-if blocks that can be flattened into this if block */
		flattened?: IfBlock[];    // 分析阶段填充:可展平的 else-if 链
		expression: ExpressionMetadata;
	};
}

这里有两个值得注意的设计:

  1. elseif 标记{#if x}{:else if y} 中的第二个条件在 AST 里并非平级兄弟节点,而是一个 elseif: true 的 IfBlock,嵌套在外层 IfBlock 的 alternate 片段中。{:else} 则直接替换 alternate 为普通 Fragment。
  2. metadata.flattened:分析阶段会把可合并的 else-if 块"摊平"挂到外层 IfBlock 上,供转换阶段一次性生成等价的 if 语句链——这是理解编译输出的关键(见第四节)。

三、解析阶段:三个函数决定 if 块的合法性

if 块的词法/语法解析全部位于 tag.js 中,由 opennextclose 三个函数分别处理 {#if ...}{:else...}{/if} 三类标签。

3.1 {#if ...} 的打开(open 函数)

open() 中,解析器在 {# 后匹配到 if 关键字时:

  • 要求 if 之后必须跟随空白parser.require_whitespace())——即 {#if x} 合法而 {#if(x)} 不合法;
  • 调用 read_expression 读取条件表达式,构造 type: 'IfBlock'elseif: falsealternate: null 的节点,并压入解析器栈、把 consequent 片段作为当前输出目标。

3.2 {:else} / {:else if} 的续接(next 函数)

next() 针对 IfBlock 的处理体现了两处重要的错误拦截:

if (block.type === 'IfBlock') {
	if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
	if (parser.eat('if')) e.block_invalid_elseif(start);
	// ...
	// :else if
	if (parser.eat('if')) {
		// 构造 elseif: true 的子 IfBlock,压栈并切换输出片段
	}
	// 否则为 :else,创建 alternate 片段
}
  • 冒号标签必须先是 else,否则报错 expected_token
  • elseif 连写是非法的:解析器刻意先吃掉 else,再检查剩余是否为 if 且前面没有空白,从而触发 block_invalid_elseif 错误——其提示语正是 "'elseif' should be 'else if'"。这是与 JavaScript 语法差异最常见的踩坑点。

匹配到 {:else if} 时,解析器读取新表达式,构造一个 elseif: true 的子 IfBlock 推入栈中(它的外层仍是原 IfBlock);匹配到 {:else} 时则创建 alternate 片段。

3.3 {/if} 的关闭与标签配对(close 函数)

close() 中 IfBlock 分支先校验 if 关键字配对,随后:

while (block.elseif) {
	block.end = parser.index;
	parser.stack.pop();
	block = parser.current();
}

这个循环的含义是:多个 {:else if} 在 AST 上是层层嵌套的,闭合时会沿 elseif: true 链一路弹出,使整条 if/else-if 链在结构上"收敛"到最外层 IfBlock。若遇到不匹配的闭合标签,则报 block_unexpected_close("Unexpected block closing tag");块未闭合则报 block_unclosed("Block was left open");{:...} 出现在错误位置(例如前一个元素/块未闭合)则报 block_invalid_continuation_placement

四、分析阶段:else-if 链的展平判定

进入分析阶段后,IfBlock 访问器 完成三件事:

  1. 空块校验validate_block_not_empty 分别检查 consequentalternate。从源码看(shared/utils.js),只有当块内恰好只有一个纯空白文本节点时才发出 block_empty 警告——注释中说明这是为了避免开发者输入到一半时被警告打扰;
  2. 动态标记:调用 mark_subtree_dynamic(context.path),把 if 块所在的模板子树标记为动态。从源码结构看,该标记会参与后续"模板能否整体静态化/提升"之类的优化判断;
  3. else-if 展平(核心逻辑):
// Check if we can flatten branches
const alt = node.alternate;

if (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
	const elseif = alt.nodes[0];

	// Don't flatten if this else-if has an await expression or new blockers
	if (
		!elseif.metadata.expression.has_await &&
		!elseif.metadata.expression.has_more_blockers_than(node.metadata.expression)
	) {
		node.metadata.flattened = [elseif, ...(elseif.metadata.flattened ?? [])];
		elseif.metadata.flattened = undefined;
	}
}

展平条件有三条,缺一即保留嵌套结构:

  • alternate 片段中只有一个节点且是 elseif: true 的 IfBlock(即源码上就是 {:else if} 写法,而不是 {:else} 里手写一个 {#if});
  • 该分支条件不含 await 表达式
  • 该分支条件没有引入新的"阻塞器"(如额外的响应式依赖,has_more_blockers_than 用于比较两个表达式之间的阻塞依赖是否增多)。

满足条件的 else-if 块被依次收集进外层 IfBlock 的 metadata.flattened 数组(链式展平:每层递归合并下一层的 flattened),从而在编译期把整条 if/else-if 链当作一条语句链处理。

五、编译输出:客户端与服务端

5.1 客户端:$.if + 分支函数 + 分支键

客户端 IfBlock 转换器 会把整条链编译为运行时 $.if(anchor, fn) 调用,核心结构为:

// 伪代码化的编译产物结构
var consequent_1 = ($$anchor) => { /* #if 分支体 */ };
var consequent_2 = ($$anchor) => { /* else if 分支体 */ };
var alternate  = ($$anchor) => { /* else 分支体 */ };

$.if(anchor, ($$render) => {
	if (/* test */) {
		$$render(consequent_1);          // 分支键 0
	} else if (/* test2 */) {
		$$render(consequent_2, 1);       // 分支键 1
	} else {
		$$render(alternate, -1);         // else 固定使用键 -1
	}
});

几个实现细节值得展开:

  • 分支体被编译为带 $$anchor 参数的箭头函数,通过 $$render(fn, key) 激活;分支键从 0 递增,else 分支固定为 -1(见 IfBlock.js L55-L75)。
  • 条件含函数调用时包一层 $.derived:若 branch.metadata.expression.has_call 为真,test 表达式会先包进 $.derived(() => ...)$.get 读取——从源码结构看,这是为了让带副作用调用/非纯条件的分支在响应式系统中以派生值缓存,避免重复求值。
  • 条件含 await 时整条链包进 $.async:若 has_await || has_blockers,所有语句被包入 $.async(anchor, blockers, [thunk], (anchor, $$condition) => {...}),此时 test 直接变为 $.get($$condition),等待由异步机制统一调度(对应 {:else if} 中含 await 时不展平的约束)。
  • {:else if} 写法传递 elseif 标记:当外层块是 elseif 来源时,$.if 的第三个实参传 true源码中的注释特别说明了一个微妙的行为差异——
{#if x} ... {:else} {#if y} <div transition:foo>...{/if} {/if}

{#if x} ... {:else if y} <div transition:foo>... {/if}

在逻辑上等价,但对**过渡动画(transitions)**而言并不相同:前者过渡只在 y 变化时播放,后者在 xy 变化时都应播放,因此 elseif 写法下运行时会将分支效果标记为"透明"(EFFECT_TRANSPARENT)。

5.2 运行时:BranchManager 与分支键

$.if 的实现在 dom/blocks/if.js(由 internal/client/index.jsif_block as if 导出):

export function if_block(node, fn, elseif = false) {
	// ...
	var branches = new BranchManager(node);
	var flags = elseif ? EFFECT_TRANSPARENT : 0;

	function update_branch(key, fn) {
		if (hydrating) {
			var data = read_hydration_instruction(marker);
			// "[n" = branch n, "[-1" = else
			if (key !== parseInt(data.substring(1))) {
				// Hydration mismatch: remove everything inside the anchor and start fresh.
				// This could happen with `{#if browser}...{/if}`, for example
				// ...
			}
		}
		branches.ensure(key, fn);
	}

	block(() => {
		var has_branch = false;
		fn((fn, key = 0) => {
			has_branch = true;
			update_branch(key, fn);
		});
		if (!has_branch) {
			update_branch(-1, null);   // 所有条件都不满足 → 清理分支
		}
	}, flags);
}

机制要点:

  • if 块被包进一个响应式 block() 效果。条件变化触发效果重跑时,fn 内部会重新执行编译产物里的 if 语句链,最终调用 branches.ensure(key, fn)挂载键对应的分支体,卸载其余分支。这是"分支切换时旧分支销毁、新分支创建"这一行为的实现源头,也因此 if/else 切换默认会销毁分支内的状态(子组件实例、effect 等)。
  • 若所有条件均不满足,调用 update_branch(-1, null),即不挂载任何分支。
  • 水合(hydration)hydrating 状态下会读取服务端留下的指令注释 "[n"(第 n 个分支)或 "[-1"(else),若客户端当前要挂载的分支键与服务端不一致,说明发生了水合失配——注释中给出的典型场景正是 {#if browser}...{/if} 这类服务端/客户端条件天然不同的写法。此时客户端会清空锚点内节点并全量重渲染,避免 DOM 状态错乱。

5.3 服务端:原生 if 语句 + 水合锚点

服务端 IfBlock 转换器 的产出简单直接:一条标准 JavaScript if 语句链,每个分支体前插入 HTML 注释形式的分支标记

prepend_block_marker(consequent, `<!--[0-->`);   // 第 0 分支
prepend_block_marker(branch, `<!--[1-->`);      // 第 1 分支(else if,依次递增)
prepend_block_marker(final_alternate, `<!--[-1-->`); // else

这些 <!--[n--> 注释正是客户端水合时 read_hydration_instruction 读取的数据来源:服务端渲染时先输出标记再输出分支内容,客户端据此校验"服务端选中了哪个分支"。

六、易错点与工程建议

结合上述源码事实,整理如下可直接核对的要点:

现象/写法 编译器行为 依据
{#elseif x}(连写) 报错 block_invalid_elseif:"'elseif' should be 'else if'" tag.js next()template.md
{/if} 不配对 / 块未闭合 报错 block_unexpected_close / block_unclosed template.md
{:else} 出现在未闭合元素内 报错 block_invalid_continuation_placement template.md
空块 {#if x}{/if}(仅空白) 发出 block_empty 警告(而非错误) shared/utils.js
{:else}{#if y}... vs {:else if y} 逻辑等价,但分支内过渡动画的触发条件不同(后者将 xy 变化都视为"局部"变化) client/IfBlock.js 注释
else-if 条件含 await 或更多阻塞依赖 该分支不参与展平,保留嵌套结构,整链走 $.async 路径 analyze/IfBlock.js

行为验证可参考仓库中的运行时测试样例:if-block-dependenciesif-dependency-orderif-nested-templateasync-if-else(await 条件分支)、if-transition-inertif-transition-undefined(过渡与 if 块的交互)。

七、总结与延伸阅读

{#if} 块是 Svelte 模板中控制流的基本单位,其完整生命周期为:tag.js 解析为嵌套 IfBlock 节点 → 分析阶段校验并展平 else-if 链(metadata.flattened)→ 客户端编译为 $.if(anchor, fn, elseif?) 调用并由 BranchManager 管理分支挂载/卸载,服务端则编译为原生 if 语句链并输出 <!--[n--> 水合锚点。理解"分支键(0, 1, ..., -1)"与"水合指令注释"这两条线索,即可解释 if 块切换时状态销毁、服务端/客户端条件失配等全部底层行为。

相关文档:03-each.md(列表渲染)、04-key.md(key 块与状态保留)、05-await.md(await 块)、18-class.md(class 指令与条件类名——简单的类名切换优先用 {#if} 之外的 class:directive)。错误码全文见 30-compiler-errors.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384