前言

Control Flow 是 Angular v17 版本后推出的新模板语法,用来取代 NgIf、NgForOf、NgSwitch 这 3 个 Structure Directive。

Structure Directive 的好处是比较灵活,原理简单,但是即便用了微语法,它看上去还是相当繁琐,而且不够优雅。

Conrol Flow 的好处是它的语法够美,缺点是不必 Structure Directive 灵活,开发者无法做任何 customize,只能看 Angular 给什么用什么。

参考

Docs – Built-in control flow

Docs – Deferrable Views

@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 个点非常强

  1. trackByFunction 不需要在 AppComponent 里定义了

  2. @empty 取代了 NgIf 指令

@for 的原理我们就不翻源码了,它和 @if 一样都不依赖 Structure Directive,里面都是调用底层的接口,也因为这样 @for 比 NgForOf 指令速度快很多哦。

@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。

  1. @defer (on idle)

    idle 指的是游览器的 requestIdleCallback 事件

  2. @defer (on immediate)

    immediate 表示一旦 Angular first render 结束后立马就触发。

  3. @defer (on timer(500ms))

    timer 就是 setTimeout,可以写 ms (millisecond) 或者 s (second)

  4. @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。

  5. @defer(on viewport)

    viewport 指的是当指定的 element 出现在 viewport 里,它是通过 IntersectionObserver 实现的。

    和 on hover 一样,默认的 element 是 @placeholder,我们可以通过传入参数指定 element。

  6. @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

Angular 18+ 高级教程 – Component 组件 の Control Flow的更多相关文章

  1. Control Flow 如何处理 Error

    在Package的执行过程中,如果在Data Flow中出现Error,那么Data Flow component能够将错误行输出,只需要在组件的ErrorOutput中进行简单地配置,参考<D ...

  2. vue 基础-->进阶 教程(3):组件嵌套、组件之间的通信、路由机制

    前面的nodejs教程并没有停止更新,因为node项目需要用vue来实现界面部分,所以先插入一个vue教程,以免不会的同学不能很好的完成项目. 本教程,将从零开始,教给大家vue的基础.高级操作.组件 ...

  3. Angular CLI 使用教程指南参考

    Angular CLI 使用教程指南参考 Angular CLI 现在虽然可以正常使用但仍然处于测试阶段. Angular CLI 依赖 Node 4 和 NPM 3 或更高版本. 安装 要安装Ang ...

  4. 一篇文章看懂angularjs component组件

     壹 ❀ 引 我在 angularjs 一篇文章看懂自定义指令directive 一文中详细介绍了directive基本用法与完整属性介绍.directive是个很神奇的存在,你可以不设置templa ...

  5. SSIS的 Data Flow 和 Control Flow

    Control Flow 和 Data Flow,是SSIS Design中主要用到的两个Tab,理解这两个Tab的作用,对设计更高效的package十分重要. 一,Control Flow 在Con ...

  6. 分享25个新鲜出炉的 Photoshop 高级教程

    网络上众多优秀的 Photoshop 实例教程是提高 Photoshop 技能的最佳学习途径.今天,我向大家分享25个新鲜出炉的 Photoshop 高级教程,提高你的设计技巧,制作时尚的图片效果.这 ...

  7. SSIS ->> Control Flow And Data Flow

    In the Control Flow, the task is the smallest unit of work, and a task requires completion (success, ...

  8. [译]Stairway to Integration Services Level 9 - Control Flow Task Errors

    介绍 在本文中,我们会实验 MaximumErrorCount和ForceExecutioResult 故障容差属性,并且还要学习Control Flow task errors, event han ...

  9. Asynchronous JS: Callbacks, Listeners, Control Flow Libs and Promises

    非常好的文章,讲javascript 的异步编程的. ------------------------------------------------------------------------- ...

  10. Chapter 8: Exceptional Control Flow

    概述: 我们可以用一种“流”的概念来理解处理器的工作流程,PC(Program Counter)依次为a0,a1,a2,...,an-1,这个序列可以称作control flow.当然我们并不总是按顺 ...

随机推荐

  1. django 中的collectstatic

    django 中的collectstatic 在Django中,"collectstatic"是一个管理命令,用于收集和复制项目中的静态文件到一个指定的静态文件目录,以便于部署. ...

  2. [oeasy]python0145_版本控制_git_备份还原

    git版本控制 回忆上次内容 上次我们了解了 try 的完全体 try 尝试运行   except 发现异常时运行的代码块   else 没有发现异常时运行的代码块   finally 无论是否发现异 ...

  3. 简单的字符串处理函数_C语言

    字符串数组 // Code file created by C Code Develop #include "stdio.h" #include "stdlib.h&qu ...

  4. Vue 新增不参与打包的接口地址配置文件

    Vue 新增不参与打包的接口地址配置文件   by:授客 QQ:1033553122   开发环境   Win 10   Vue 2.5.2 问题描述 vue工程项目,npm run build we ...

  5. 题解:P7482 不条理狂诗曲

    题解:P7482 不条理狂诗曲 本题解借鉴 blossom_j 大佬思路,但这位大佬的题解似乎没放正确代码. 题意 对于每一个 \(a\) 的子区间 \(a_{l\dots r}\),求选择若干个不连 ...

  6. mysql 忘记root密码怎么办?

    忘记root可以跳过grant table来登录 1.打开命令行输入以下命令 mysqld -nt --grant-skip-tables 2.在打开一个新命令行,输入以下命令可以登录, mysql ...

  7. Android Spingboot 实现SSE通信案例

    SSE SSE(Server-Sent Events)是一种用于实现服务器主动向客户端推送数据的技术,它基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接 ...

  8. 【VMware VCF】VMware Cloud Foundation Part 04:准备 ESXi 主机。

    VMware Cloud Foundation 管理域部署要求至少准备 4 台 ESXi 主机作为最小计算单元,如果采用整合部署(管理域和 VI 工作负载域合并),还需要根据实际情况适量增加 ESXi ...

  9. 修改PE文件来实现管理员权限

    在Windows我们常用的方法就是给应用添加app.manifest清单文件,然后生成的Exe就会具有管理员权限. 近期我在使用Wix制作Exe安装包时,发现此方法不通,我在github上和Stack ...

  10. ipa文件上传到app store的构建版本的工具

    打包好ipa文件后,可以使用mac电脑上的xcode将ipa上传到app store的构建版本中,假如没有mac电脑,可以使用香蕉云编来将ipa文件上传到构建版本. 这里我们来介绍下ipa文件上传到a ...