Angular 模板控制流深入指南:全面解析 `@if`、`@for` 与 `@switch` 块
本篇指南以 Angular 官方模板指南中的控制流文档为核心,系统讲解 Angular 模板中用于「条件显示、列表循环与分支匹配」的内置块语法 @if / @for / @switch(含 @else if、@empty、@default 等配套块)。文章不仅覆盖每种块语法的完整写法、上下文变量与 track 性能语义,还会结合当前仓库中编译器(packages/compiler)与运行时(packages/core)的真实实现源码,说明这些语法在编译期如何被解析、在运行期如何被渲染,帮助你既能正确写出模板代码,也能理解其底层原理。
本文对应的原始指南位于 adev/src/content/guide/templates/control-flow.md,建议与同目录下的 变量声明指南 @let 一起阅读。
一、为什么需要块(Block)语法
Angular 模板支持控制流块,用于在模板中有条件地显示、隐藏和重复渲染元素。与传统的结构型指令(如 *ngIf、*ngFor、*ngSwitch)不同,块语法直接内嵌在模板的标记中,具备以下特点:
- 书写直观:不再需要星号前缀与引号包裹,代码结构与 HTML 对齐;
- 类型检查更强:块内作用域清晰,配合 Angular 的模板类型检查器可以做穷尽性校验(见下文
@switch部分); - 性能更好:如
@for基于track复用视图而非销毁重建。
块语法由模板解析器识别并以 @ 开头,完整的可用块还包括 @defer 延迟加载块 与 @let 变量声明(见 variables.md)。
二、用 @if、@else if、@else 条件显示内容
@if 块在其条件表达式为真值(truthy)时,条件显示其内部内容:
@if (a > b) {
<p>{{ a }} is greater than {{ b }}</p>
}
如需显示替代内容,可在其后跟任意数量的 @else if 块以及一个(且仅一个)@else 块:
@if (a > b) {
{{ a }} is greater than {{ b }}
} @else if (b > a) {
{{ a }} is less than {{ b }}
} @else {
{{ a }} is equal to {{ b }}
}
从编译器实现看,
@else if与@else并不会生成独立的 AST 节点,而是由createIfBlock将紧跟其后的连接块收集为同一个IfBlock的多个分支(branch)。编译器用谓词函数isConnectedIfLoopBlock判断else/else if是否合法地跟在@if之后;若把@else单独放在其它位置,模板转换器 会抛出诸如 “@elseblock can only be used after an@ifor@else ifblock.” 的编译错误。
引用条件表达式的结果(as 别名)
@if 支持把条件表达式的结果保存到变量中,供块内部复用:
@if (user.profile.settings.startDate; as startDate) {
{{ startDate }}
}
当模板中需要多次引用较长的表达式时,这种写法可读性与可维护性更好。语法为在条件表达式后用分号 ; 分隔,再接 as 关键字与别名。编译器通过 CONDITIONAL_ALIAS_PATTERN(/^(as\s+)(.*)/)解析该别名。
运行期行为:分支匹配与视图复用
在运行时层面,@if 会被编译成对指令 ɵɵconditional 的调用。其核心逻辑是:
- 按顺序求值各分支条件,得到匹配的模板下标
matchingTemplateIndex; - 若所有条件都不为真(
matchingTemplateIndex为-1),则不显示任何视图; - 命中分支时,通过
createAndRenderEmbeddedLView创建内嵌视图并加入容器;若条件变化导致切换分支,会先把旧分支的LView从容器移除(见 control_flow.ts)。
需要注意:当条件从真变为假、又从假变回真时,Angular 会重新渲染对应分支的内容(即 @if 本身不具备缓存真分支视图的能力)。
三、用 @for 块循环渲染内容
@for 块遍历一个集合并反复渲染块内的内容。该集合可以是任意 JavaScript 可迭代对象(iterable),但 Angular 对 Array 类型做了额外的性能优化。
一个典型的 @for 循环如下:
@for (item of items; track item.id) {
{{ item.name }}
}
需要特别说明的是:@for 块不支持类似 JavaScript continue 或 break 的流程控制语句。如果循环过程中需要跳过某些元素,应在数据源层面过滤后再交给模板(例如在组件中通过 items.filter(...) 派生新数组)。
从编译器侧看,@for 的语法由两个正则约束:FOR_LOOP_EXPRESSION_PATTERN(/^\s*([0-9A-Za-z_$]*)\s+of\s+([\S\s]*)/,用于解析 item of items)与 FOR_LOOP_TRACK_PATTERN(/^track\s+([\S\s]*)/,用于解析 track 表达式),两者定义于 r3_control_flow.ts。
为什么 track 对 @for 如此重要?
track 表达式允许 Angular 维护「数据 ↔ 页面 DOM 节点」之间的对应关系。当数据变化时,Angular 依据 track 的结果只执行必要的 DOM 操作(最小化增删改),从而优化渲染性能。正确使用 track 可以显著提升循环渲染大量数据时的性能。
选择 track 表达式时请遵循以下优先级:
- 优先选择能唯一标识每条数据的属性。如果你的数据模型包含唯一标识属性(常见命名是
id或uuid),就使用它;如果数据模型没有这类字段,强烈建议为数据补充一个; - 对永不变化的静态集合,可使用内置变量
$index按索引跟踪; - 没有其它选择时才可把条目本身作为跟踪键——即用三重等号
===按引用同一性跟踪。这种方式应尽量避免,因为当 Angular 无法判断哪条数据对应哪个 DOM 节点时,会显著拖慢更新渲染:
@for (item of items; track item) {
{{ item.name }}
}
与
*ngFor的关键差异:@for块优先复用视图(view reuse)。如果被跟踪的属性发生变化、但对象引用不变,Angular 会直接更新视图的绑定(包括组件输入),而不是销毁并重建整个元素。这一点在官方文档中以 NOTE 形式强调。运行时的证据来自 packages/core/src/render3/instructions/control_flow.ts:当使用索引跟踪时编译器会直接复用内置函数
ɵɵrepeaterTrackByIndex(返回下标),按引用跟踪时则复用ɵɵrepeaterTrackByIdentity(返回值本身)。把这两种常用跟踪函数内置在运行时,可以避免为每个模板重复生成函数体。
@for 块的上下文变量
在 @for 块内部,始终可以访问以下隐式变量:
| 变量 | 含义 |
|---|---|
$count |
被遍历集合中的元素数量 |
$index |
当前行的下标 |
$first |
当前行是否为第一行 |
$last |
当前行是否为最后一行 |
$even |
当前行下标是否为偶数 |
$odd |
当前行下标是否为奇数 |
这些变量名称固定,但可以通过 let 片段为它们设置别名:
@for (item of items; track item.id; let idx = $index, e = $even) {
<p>Item #{{ idx }}: {{ item.name }}</p>
}
别名在嵌套 @for 块时尤其有用:内层 @for 会遮蔽同名隐式变量,通过别名你可以在内层读取外层 @for 的上下文变量。
编译器在 r3_control_flow.ts 中维护了一个只读集合
ALLOWED_FOR_LOOP_LET_VARIABLES,即$index、$first、$last、$even、$odd、$count。在let别名中引用这六个名字以外的变量,会被编译器直接拒绝。运行时侧,这些变量来自
RepeaterContext类(见 control_flow.ts):每个被复用的行视图会携带一个上下文对象,其中$implicit对应当前数据项item、$index为当前下标,而$count是通过lContainer.length - CONTAINER_HEADER_OFFSET动态计算的——这正是为什么无论把@for放在模板哪个层级,$count都能反映真实遍历条数。
用 @empty 块提供空集合的兜底
你可以在 @for 块内容结束后紧接一个可选的 @empty 段。当集合中没有元素时,@empty 块的内容会被显示:
@for (item of items; track item.name) {
<li>{{ item.name }}</li>
} @empty {
<li>There are no items.</li>
}
@empty 本质上替代了过去需要 *ngIf="items.length === 0" 的写法,让「空态」与「列表」在结构上天然相邻。
编译器同样对该块的位置做了约束:谓词 isConnectedForLoopBlock 只允许名为 empty 的块连接在 @for 之后;若 @empty 脱离 @for 单独出现,模板转换会报错 “@empty block can only be used after an @for block.”(见 r3_template_transform.ts)。运行时通过 RepeaterMetadata.hasEmptyBlock 记录是否存在空态块,集合为空时直接渲染该分支。
四、用 @switch 块按分支渲染
@if 能覆盖绝大多数条件场景,但当你有多个互斥分支需要匹配同一表达式时,@switch 提供了更贴近 JavaScript switch 语句的另一种写法:
@switch (userPermissions) {
@case ('admin') {
<app-admin-dashboard />
}
@case ('reviewer')
@case ('editor') {
<app-editor-dashboard />
}
@default {
<app-viewer-dashboard />
}
}
使用 @switch 时有以下几点语义需要牢记:
- 比较采用严格相等(
===):@switch的条件表达式值与每个@case的表达式值之间使用三重等号比较,不存在 JavaScriptswitch的隐式类型转换; @switch没有穿透(fallthrough)行为:一旦某个@case匹配,其块内容渲染后即结束,不需要也不存在break/return之类的语句。这消除了 JavaScriptswitch中“忘写break导致意外穿透”的经典 bug 来源;- 多个
@case共享同一块内容:通过连续书写多个@case语句,可以让它们落入同一个渲染块(如上面'reviewer'与'editor'共用一个 dashboard); @default作为兜底:当所有@case都不匹配时显示@default块的内容;若既无匹配的@case、又没有@default块,则什么都不显示。
穷尽性类型检查(Exhaustive Type Checking)
@switch 支持穷尽性类型检查:Angular 可以在编译期验证联合类型(union type)的所有取值都已被处理。
通过在最后一个分支使用 @default never;,你显式声明“不应存在剩余未处理的情形”。若日后联合类型被扩展出一个新值、而 @case 没有覆盖它,Angular 的模板类型检查器会报告错误,从而及早发现遗漏的分支:
@Component({
template: `
@switch (state) {
@case ('loggedOut') {
<button>Login</button>
}
@case ('loggedIn') {
<p>Welcome back!</p>
}
@default never; // 会抛错,因为缺少 @case ('loading')
}
`,
})
export class AppComponent {
state: 'loggedOut' | 'loading' | 'loggedIn' = 'loggedOut';
}
关于穷尽性检查有两点使用限制:
- 依赖 TypeScript 的类型收窄(narrowing),且只对变量生效:如果
@switch的条件是一个函数调用或信号(例如@switch (state())),类型收窄无法进行,穷尽性检查不会生效。解决办法是把信号先赋给一个@let变量,例如@let mySignal = this.mySignal();(@let语法的完整说明见 变量声明指南); - 当被 switch 的表达式嵌套在联合类型内部时,需要显式向
never指定表达式,让编译器对嵌套联合的成员逐一校验:
@Component({
template: `
@switch (state.mode) {
@case ('show') {
{{ state.menu }};
}
@case ('hide') {}
@default never(state);
}
`,
})
export class App {
state!: {mode: 'hide'} | {mode: 'show'; menu: number};
}
该能力并非模板语法的糖衣,而是由模板类型检查器实现的。在仓库中可找到对应的类型检查实现,例如 switch 块类型检查算子 以及 if / for / switch 类型检查基础设施,编译期对
@switch分支的联合类型完备性做静态分析。这从侧面说明:把状态机(如登录态'loggedOut' | 'loading' | 'loggedIn')放进模板并用@switch驱动,能让“遗漏分支”在编译阶段就被拦截。
五、实践建议与相关资料
- 列表渲染先谈
track,再谈功能:为数据模型添加稳定的id/uuid字段是让@for发挥视图复用性能的前提;跨集合或身份标识不稳定的数据(如无 key 的原始值数组)谨慎直接track item。 - 空态优先用
@empty:把“无数据”界面直接声明在列表旁边,比单独的*ngIf更内聚、更不易遗漏。 - 多分支互斥场景选
@switch+ 穷尽性检查:用@default never;让未来的状态枚举扩展变成“编译错误”而非“运行期白屏”。 - 复杂表达式长、需反复引用时:使用
@if (...; as x)别名或@let变量提升可读性。
如果想继续深入了解相关主题,可以顺藤摸瓜阅读以下同仓库资料:
- 模板变量与
@let语法:块内临时变量的声明方式; - 模板中的表达式语法:块条件表达式内可用的表达式能力与限制;
@defer延迟渲染块:本系列块语法中的另一成员;- 模板与结构指令对照资料:
ng-template、ng-container、ng-content; - 编译端实现:r3_control_flow.ts(语法解析与校验)、r3_template_transform.ts(块到指令的转换与错误上报);
- 运行端实现:packages/core/src/render3/instructions/control_flow.ts(
@if分支匹配、RepeaterContext与内置 track 函数)。
总体而言,@if / @for / @switch 块把 Angular 模板中最高频的三种控制流需求沉淀为语言级语法,再配合严格的编译期解析、类型检查与运行期视图复用,是当前在 Angular 模板中编写条件与循环逻辑的首选方式。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00