CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-177 · Angular 高级教程

Angular 22+ 教程 08:新的控制流 @if / @for / @switch

条件渲染与列表渲染是模板的骨架。Angular v17 引入的块状控制流 @if / @for / @switch 在 v22 已是绝对主角:更少的 import、更严的类型检查、更贴近现代语言的语法。本文对照旧的结构型指令(*ngIf / *ngFor / *ngSwitch)讲清新语法的设计动机与全部细节,并补上 @defer 这一延迟加载利器。

为什么要有新的控制流

旧写法的三个痛点:

<!-- 需要 import:NgIf / NgFor 要进组件的 imports(早期是 CommonModule) -->
<li *ngFor="let todo of todos; trackBy: trackById; index as i; odd as isOdd">
  {{ i + 1 }}. {{ todo.title }}
</li>
<li *ngIf="todos.length === 0">清单是空的</li>
  1. 结构型指令本质是指令,必须导入才能用,IDE 自动补全经常漏。
  2. 微语法(let x of xs; trackBy: f; index as i)是字符串 DSL,语义靠文档约定。
  3. 类型收窄能力弱:*ngIf="user" 后模板里的 user 未必被收窄为非空。

新控制流是编译器原生语法——不需要 import、没有微语法、类型收窄一流:

*ngIf="cond"@if (cond) { }
*ngIf="cond; else tpl"@if (cond) { } @else { }
*ngFor="let x of xs; trackBy: f"@for (x of xs; track x.id) { }
*ngSwitch 三件套@switch / @case / @default
无对应@defer@empty@else if

@if / @else if / @else

@if (order().status === 'paid') {
  <p class="ok">已支付,感谢惠顾</p>
} @else if (order().status === 'pending') {
  <p>等待支付……</p>
} @else {
  <p>订单已取消</p>
}

两个进阶能力:as 别名与类型收窄。

type User = { name: string; email?: string };
user = signal<User | null>(null);
@if (user(); as u) {
  <!-- u 已收窄为 User,且在同一块内稳定复用 -->
  <p>{{ u.name }}:{{ u.email ?? '未留邮箱' }}</p>
}

as u 把求值结果绑定到局部别名,同时完成非空收窄——过去要靠 *ngIf="user as u; else empty" 加层层嵌套模板才能实现的效果,现在一行搞定。块内还支持写 HTML 注释,模板的可读性维护手段不再受限。

@for 与必须的 track

<ul>
  @for (todo of todos(); track todo.id) {
    <li [class.done]="todo.done">{{ todo.title }}</li>
  } @empty {
    <li class="empty">清单是空的,先加一条吧</li>
  }
</ul>

三件事必须记住:

**1)track 是强制的。**不写 track 编译直接报错。track 是一个表达式,求值结果作为列表项的身份标识:

@for (todo of todos(); track todo.id) { }       <!-- 推荐:稳定业务 id -->
@for (item of items(); track $index) { }        <!-- 兜底:下标 -->

2)track 与旧 trackBy 的区别:trackBy 是”函数返回 key”,track 直接是”算 key 的表达式”,省掉样板函数:

// 旧写法需要它
trackById(_: number, todo: Todo) { return todo.id; }

**3)用稳定 id,谨慎用 $index。**列表会重排、增删中间项时,track $index 会导致框架认为”每个位置的内容变了”,DOM 复用退化。只在”只追加、不重排”的列表(日志流)上使用 $index 才安全。

@for 自带一批隐含变量,都在块内可用:

变量含义
$index当前下标
$count列表长度
$first / $last是否首/末项
$even / $odd下标奇偶
@for (row of rows(); track row.id; let odd = $odd, i = $index) {
  <tr [class.odd]="odd">...</tr>
}

@empty 块在列表为空时渲染,替代了旧的三元判断,空状态文案从此有固定落点。

@switch / @case / @default

type Status = 'idle' | 'loading' | 'success' | 'error';
status = signal<Status>('idle');
@switch (status()) {
  @case ('idle') { <p>点击按钮开始</p> }
  @case ('loading') { <app-spinner /> }
  @case ('success') { <p>加载完成</p> }
  @case ('error') { <p>出错了</p> }
}

与 JS 的 switch 不同,@case 不会贯穿,也无需 break。当 @case 分支已穷尽联合类型的所有成员时,若仍书写 @default,其中再引用被匹配的表达式,其类型已是 never——TypeScript 会在编译期把误用标红,相当于免费的穷尽性检查。新写分支型业务(状态机、枚举)时,建议故意删掉一个 @case 看编译器报错,体会这层保护。

@defer:按需渲染的利器

@defer 块及其触发器(trigger)让”昂贵内容延迟出现”成为原生能力:

@defer (on viewport; prefetch on idle) {
  <app-heavy-chart [data]="series()" />
} @placeholder {
  <div class="chart-skeleton">图表位置</div>
} @loading {
  <app-spinner />
} @error {
  <p>图表加载失败</p>
}

常用触发器:

触发器时机
on idle浏览器空闲时(可带时长,如 on idle(500ms),要求空闲保持该时长后触发)
on viewport占位元素进入视口
on immediate立即
on hover / on interact悬停 / 用户交互(点击、输入)
on timer(5s)定时
when expr表达式为真
prefetch ...配合主触发器,提前预取不渲染

配套块各司其职:@placeholder(必写的初始占位,支持 minimum 参数避免闪烁)、@loading(触发后加载中)、@error(懒加载失败)。@defer 内的依赖是独立懒加载 chunk 的天然切分边界,“首屏之外的重组件”是它最典型的应用场景;SSR 下服务端会渲染 @placeholder,到客户端再按触发器接管。

嵌套与性能

块可以任意嵌套,语义清晰:

@for (group of groups(); track group.id) {
  <section>
    <h3>{{ group.name }}</h3>
    @if (group.items().length > 0) {
      @for (item of group.items(); track item.id) {
        <article>{{ item.title }}</article>
      }
    } @else {
      <p>组内暂无条目</p>
    }
  </section>
}

性能层面的三个事实:

  • Zoneless 模式下,@for 依赖的信号变化只触发该列表的重新 diff,与其他视图区域无关。
  • track 的质量直接决定 DOM 复用率,列表卡顿时第一件事就是检查 track 表达式。
  • @if 求值开销极低,“用 @if 隐藏复杂子树”依然是性价比最高的优化手段之一。

从旧语法迁移

ng update 升级到 v17+ 时,codemods 会自动把 *ngIf / *ngFor / *ngSwitch 转换为块语法,且转换质量很高(含 astrackBytrack 的映射)。旧的结构型指令本身仍可用(自定义结构型指令的场景第 12 篇展开),但新代码请统一用块语法——它不只是”换种写法”,而是类型检查、tree-shaking、可读性的全面升级。

小结

  • 块状控制流是编译器原生语法:无需 import、无微语法、类型收窄完整。
  • @if 支持 @else if / @else / as 别名;@for 必须写 track,稳定 id 优先,$index 只用于只追加列表。
  • @empty 是空状态的固定落点;@switch 无贯穿,穷尽时 @default 分支为 never,误用编译期暴露。
  • @defer + 触发器(on idle(500ms) / on viewport / when 等)+ @placeholder / @loading / @error 构成完整的延迟渲染方案。
  • 迁移交给 codemods,新代码从第一行就用新语法。

系列导航

← 算法 008:选择排序(Selection sort) 目录 开源合规 008:信息安全管理认证 - ISO27001 →
← 返回文章列表