Angular 18+ 高级教程 – Component 组件 の Control Flow
前言
Control Flow 是 Angular v17 版本后推出的新模板语法,用来取代 NgIf、NgForOf、NgSwitch 这 3 个 Structure Directive。
Structure Directive 的好处是比较灵活,原理简单,但是即便用了微语法,它看上去还是相当繁琐,而且不够优雅。
Conrol Flow 的好处是它的语法够美,缺点是不必 Structure Directive 灵活,开发者无法做任何 customize,只能看 Angular 给什么用什么。
参考
@if @else if @else
这个是 NgIf 指令的写法
<ng-template #loadingTemplate> <p>loading...</p> </ng-template> <p *ngIf="person$ | async as person; else loadingTemplate">{{ person.name }}</p>
这个是 Control Flow 的写法
@if(person$ | async; as person) { <p>{{ person.name }}</p> } @else { <p>loading...</p> }
另外,pipe async 后还可以继续判断哦,比如
@if(number$() | async; as number === 5) { <p>{{ number }}</p> }
number$ 类型是 Signal<Observable<number>>。
另外,Control Flow 还支持 @else if,这个是 NgIf 指令不支持的。
@if (value >= 5) { <p>greater than or equal to five</p> } @else if (value >= 2) { <p>greater than or equal to two</p> } @else { <p>less than two</p> }
Control Flow @if 的原理
Contorl Flow 不依赖 NgIf 指令,甚至不依赖 ViewContainerRef,它使用的是比较底层的接口,
比如 createAndRenderEmbeddedLView 函数和 addLViewToLContainer 函数。这两个函数的源码我们在 Dynamic Component 文章中研究过,
它们也是 ViewContainerRef 内部使用的。
app.component.js
在 renderView 阶段,@if、@else if、@else 分别会创建三个 ng-template。
在 refreshView 阶段,会执行 ɵɵconditional 函数。第二个参数依据判断条件它会宣传第几个 ng-template。
ɵɵconditional 函数的源码在 control_flow.ts
当 @If 遇上 <ng-content select />
为什么要提这个?
the problem...
因为它是一个 Issue – Angular 17 New Control Flow | Allow more than one node at root @if, @else, @for, etc
App Template
<app-test> @if (true) { <h1>Hello World</h1> } </app-test>
Test Template
<ng-content select="h1"/>
上面这段代码没有任何问题,<ng-content /> 会 select 到 h1 element。
好,我们加点料
<app-test> @if (true) { <h1>Hello World</h1> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quod, reiciendis.</p> } </app-test>
在 @if 里面加多一行 <p>。
这时会出现一段 warning
意思是 <ng-content /> 无法 select 到 h1。
这个 warning 不是吓唬人的,它是真的就 select 不到 h1 了。
the theory behind の 逛 ng-content 源码
在 ng-content 文章中,我们提到过一个 Angular limitation -- Only first layer can be select。
我们逛一段源码了解一下。
App Template
<app-test> <h1>Hello World</h1> </app-test>
after compile
表面上看不出任何和 ng-content 相关的地方,但其实在 ɵɵelementStart 函数内它会做一些特别处理。
我们不深入源码,直接看 TView TNode 的结果就好。
Test TNode 和 h1 TNode 代表了
<app-test> <h1>Hello World</h1> </app-test>
它们的父子关系也被记入在 TNode 中。
那如果我们加多一个 <p> 呢?
<app-test> <h1>Hello World</h1> <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Modi, aliquid!</p> </app-test>
结果
child 代表 first child,first child 的 next 是 p。
好,上面是 App Template renderView 后的 TNode 结构。
那我们继续看 Test Template renderView 长啥样。
Test Template
<ng-content select="h1"/>
after compile
ɵɵprojectionDef 函数源码在 projection.ts
到这里就揭秘了。首先它拿 Test TNode.child。第一个传入 Test 组件的 TNode 是 h1。
接着它拿 h1 TNode.next,也就是 p TNode。
整个 for loop 就只是在 first layer child 一直 next 而已,不会去到 second layer。
分别会 loop 到的 TNode 是 <ng-container>,h1,ng-template。
所以 <ng-content> 也只能 select 到这 3 个 TNode,子孙层是 select 不到的。
@if 和 only first layer can be select 的关系
那 @if 和 上面提到的 only first layer can be select 又有什么关系呢?
<app-test> @if (true) { <h1>Hello World</h1> } <h1 *ngIf="true">Hello World</h1> </app-test>
@if h1 或者 *ngIf h1 compile 后 first layer TNode 会是 h1
所以 <ng-content> 可以 select 到 h1。
但是加入 p 以后就不同了
<app-test> @if (true) { <h1>Hello World</h1> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro, fugit!</p> } </app-test>
after compile
first layer TNode 不再是 h1 了,所以 <ng-content> 就 select 不到了。
*ngIf 其实也面对同样的问题。
<app-test> <!-- *ngIf 无法直接加 p --> <!-- <h1 *ngIf="true">Hello World</h1> --> <!-- 需要 wrap ng-container --> <ng-container *ngIf="true"> <h1>Hello World</h1> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro, fugit!</p> </ng-container> </app-test>
wrap 了一层 ng-container,first layer 自然就不可能是 h1 了。
the workaround...
目前没有什么优雅的方案,我们只能强制迎合它的 limitation,硬把 @if 拆成多个。
<app-test> @if (true) { <h1>Hello World</h1> } @if (true) { <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dicta, maiores?</p> } </app-test>
@for @empty
这个是 NgForOf 指令的写法
<ng-template #noDataTemplate> <p>no datas</p> </ng-template> <ng-container *ngIf="people.length > 0 else noDataTemplate"> <ng-container *ngFor="let person of people; trackBy: trackByName; let index = index"> <p>index: {{ index }}</p> <p>name : {{ person.name }}</p> </ng-container> </ng-container>
这个是 Control Flow 的写法
@for (person of people; track person.name; let index = $index) { <p>index: {{ index }}</p> <p>name : {{ person.name }}</p> } @empty { <p>no datas</p> }
有 2 个点非常强
-
trackByFunction 不需要在 AppComponent 里定义了
-
@empty 取代了 NgIf 指令
@for 的原理我们就不翻源码了,它和 @if 一样都不依赖 Structure Directive,里面都是调用底层的接口,也因为这样 @for 比 NgForOf 指令速度快很多哦。
nested @for get parent $index
在 *ngFor,我们可以给 index 定义不同的 variable name,这样在子层中也可以拿到父层的 index。
在 @for,我们没有了这个能力。
因为 @for 底层不是 Structure Directive,它没有自定义 variable name 的能力。
幸好,Angular 在 v18.1 推出了 @let 语法。
这样就行啦。
@switch @case @default
这个是 NgSwitch 指令的写法
<ng-container [ngSwitch]="status"> <ng-template [ngSwitchCase]="'Completed'"> <p>complete</p> </ng-template> <ng-template [ngSwitchCase]="'Pending'"> <p>pending</p> </ng-template> <ng-template ngSwitchDefault> <p>none</p> </ng-template> </ng-container>
这个是 Control Flow 的写法
@switch (status) { @case ('Completed') { <p>complete</p> } @case ('Pending') { <p>pending</p> } @default { <p>none</p> } }
提醒:@switch 和 ngSwtich 指令一样,都没有 fallthrough 的概念,match 到后会自动 break。
@defer @placeholder @loading @error
@defer 是 Control Flow 独有的,不存在 Defer 指令。
@defer 的作用是延迟显示一段内容,并且内容里包含的组件/指令/Pipe 还会延迟加载。
@defer (on timer(5s)) { <app-say-hi /> <app-hello-world /> }
上面这段表示 5 秒中后才输出 SayHi 和 HelloWorld 组件。
我们可以这样去理解,@defer 会被 compile 成 ng-template,5 秒钟后会 createEmbeddedView 然后 insert。
此外 compile 时它还会找出涉及的组件,改成使用 import('/path/component.ts') 的方式动态加载,这样可以减少 first load 的 bundle size。
app.component.js
@loading,@placeholder,@error
@defer (on timer(2s)) { <app-say-hi /> <app-hello-world /> } @placeholder { <p>blank...</p> } @loading { <p>loading...</p> } @error { <p>something wrong</p> }
@defer 的显示流程是这样:
一开始先显示 @placeholder 的内容,等 on timer 触发后,显示 @loading 内容,这时会去 lazyload -> createEmbededView -> insert,最终显示 @defer 内容。
如果 lazyload 的过程失败,那就显示 @error 内容。
minimum 和 after options
有时候 lazyload 或许会太快,loading 一闪而过体验不好,这时可以设置 after 和 minimum。
@loading(after 100ms; minimum 1s) { <p>loading...</p> }
当开始 lazyload 后,Angular 会开始计时,超过 100ms 后,如果 lazyload 还没有结束,那就会显示 @loading 内容,
如果 lazyload 在 1 秒钟内完成,它不会马上输出 @defer 内容,而是会等到 minimum 1s 后才输出。
注:after 和 minimum 是同时开始计时的。
@placeholder 也支持 minimum options,不过不支持 after 哦。
Trigger
除了 on timer 以外,还有另外 5 个 Triggers。
-
@defer (on idle)
idle 指的是游览器的 requestIdleCallback 事件
-
@defer (on immediate)
immediate 表示一旦 Angular first render 结束后立马就触发。
-
@defer (on timer(500ms))
timer 就是 setTimeout,可以写 ms (millisecond) 或者 s (second)
-
@defer (on hover)
<div #hoverArea>hover area</div> @defer (on hover(hoverArea)) { <app-say-hi /> <app-hello-world /> } @defer(on hover) { <app-say-hi /> <app-hello-world /> } @placeholder { <div>hover area</div> }
hover element 默认是 @placeholder 内容,我们可以传入 element 作为指定的 hover area。
-
@defer(on viewport)
viewport 指的是当指定的 element 出现在 viewport 里,它是通过 IntersectionObserver 实现的。
和 on hover 一样,默认的 element 是 @placeholder,我们可以通过传入参数指定 element。
-
@defer (on interaction)
interaction 指的是 click 和 keydown 事件,和 on hover / on viewport 一样可以指定 element。
我们可以写 multiple trigger,它的关系是 "or",也就是说只要其中一个 trigger 触发就执行。
@defer (on viewport; on timer(5s)) { <app-say-hi /> <app-hello-world /> }
当 on viewport 或者超过 5 秒后显示。
Prefetching
prefetching 的作用是多一个 trigger 用于是否提早 lazyload。
@defer (on viewport; prefetch on timer(5s)) { <app-say-hi /> <app-hello-world /> } @placeholder { <p>placeholder</p> }
5 秒后,虽然 element 没有 on viewport 但会先去加载,只加载不显示,一直到 element on viewport 才显示。
prefetch 也可以写 multiple trigger,只要其中一个触发就执行
@defer (on viewport; on timer(5s); prefetch on timer(3s); prefetch on hover) { <app-say-hi /> <app-hello-world /> } @placeholder { <p>placeholder</p> }
总结
Control Flow 代码比起 Structure Directive + 微语法整齐很多,代价是无法扩展。
NgIf、NgForOf、NgSwtich 指令相信在不久的将来会被淘汰,但是 ng-template、Structure Directive 和微语法是不会被淘汰的,
而要掌握好 Structure Directive 和微语法,我个人觉得 NgIf、NgForOf、NgSwtich 指令是非常值得参考的。
目录
上一篇 Angular 18+ 高级教程 – Component 组件 の Structural Directive (结构型指令) & Syntax Reference (微语法)
下一篇 Angular 18+ 高级教程 – Component 组件 の @let Template Local Variables
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻