首页
/ Angular 模板控制流深入指南:全面解析 `@if`、`@for` 与 `@switch` 块

Angular 模板控制流深入指南:全面解析 `@if`、`@for` 与 `@switch` 块

2026-09-06 19:06:24作者:劳婵绚Shirley

本篇指南以 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 单独放在其它位置,模板转换器 会抛出诸如 “@else block can only be used after an @if or @else if block.” 的编译错误。

引用条件表达式的结果(as 别名)

@if 支持把条件表达式的结果保存到变量中,供块内部复用:

@if (user.profile.settings.startDate; as startDate) {
  {{ startDate }}
}

当模板中需要多次引用较长的表达式时,这种写法可读性与可维护性更好。语法为在条件表达式后用分号 ; 分隔,再接 as 关键字与别名。编译器通过 CONDITIONAL_ALIAS_PATTERN/^(as\s+)(.*)/)解析该别名。

运行期行为:分支匹配与视图复用

在运行时层面,@if 会被编译成对指令 ɵɵconditional 的调用。其核心逻辑是:

  1. 按顺序求值各分支条件,得到匹配的模板下标 matchingTemplateIndex
  2. 若所有条件都不为真(matchingTemplateIndex-1),则不显示任何视图;
  3. 命中分支时,通过 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 continuebreak 的流程控制语句。如果循环过程中需要跳过某些元素,应在数据源层面过滤后再交给模板(例如在组件中通过 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 表达式时请遵循以下优先级:

  1. 优先选择能唯一标识每条数据的属性。如果你的数据模型包含唯一标识属性(常见命名是 iduuid),就使用它;如果数据模型没有这类字段,强烈建议为数据补充一个;
  2. 对永不变化的静态集合,可使用内置变量 $index 按索引跟踪;
  3. 没有其它选择时才可把条目本身作为跟踪键——即用三重等号 === 按引用同一性跟踪。这种方式应尽量避免,因为当 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 的表达式值之间使用三重等号比较,不存在 JavaScript switch 的隐式类型转换;
  • @switch 没有穿透(fallthrough)行为:一旦某个 @case 匹配,其块内容渲染后即结束,不需要也不存在 break / return 之类的语句。这消除了 JavaScript switch 中“忘写 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';
}

关于穷尽性检查有两点使用限制:

  1. 依赖 TypeScript 的类型收窄(narrowing),且只对变量生效:如果 @switch 的条件是一个函数调用或信号(例如 @switch (state())),类型收窄无法进行,穷尽性检查不会生效。解决办法是把信号先赋给一个 @let 变量,例如 @let mySignal = this.mySignal();@let 语法的完整说明见 变量声明指南);
  2. 当被 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 变量提升可读性。

如果想继续深入了解相关主题,可以顺藤摸瓜阅读以下同仓库资料:

总体而言,@if / @for / @switch 块把 Angular 模板中最高频的三种控制流需求沉淀为语言级语法,再配合严格的编译期解析、类型检查与运行期视图复用,是当前在 Angular 模板中编写条件与循环逻辑的首选方式。

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

项目优选

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