前言

读这么多原理,到底为了什么?真实项目中真的会用得到吗?

你正在疑惑 "知识的力量" 吗?

本篇会给一个非常非常好的案例,让你感悟 -- 知识如何用于实战。

记住,我的目的是让你感悟,而不是要你盲目相信知识。

很久很久以前的问题 (疑难杂症)

下面是我在 2020-11-06 记入的一个问题。

一模一样的问题也有人在 Github 提问 Github Issue – QueryList not sorted according to the actual state (提问于:2021-06-08)

这个 Issue 很特别,它没有被关闭,也没有任何的回复,提问者也没有继续追问。

没有被关闭是因为,它是一个事实,而且直觉告诉他们 (Angular Team) 这可能是一个 Bug 或者是一个可以尝试去调查的 Issue。

没有回复是因为,他们 (Angular Team) 没有一眼看出原因,然后他们懒得去调查。

提问者没有追问是因为,这个问题可以避开,并不是非要解决不可的问题。

从这里我们也可以看出 Angular 社区 (Angular Team and User) 对待 Issue 的态度。

所以,千万不要把那群人 (Angular Team) 想得太高,其实他们和你周围的工作伙伴 level 差不多而已。

如何对待这类问题?(疑难杂症)

首先,如果你是 Angular 新手,那你不会遇到这类问题,因为你用的不深。

如果你不是新手,但项目不够复杂,那你也不会遇到这类问题,还是因为你用的不够深。

当有一天你遇到这类问题的时候,如果你不够等级,那你只能傻傻的 debug 半天,找半天的资料,最后傻傻的去提问。

几天后,等了一个寂寞,于是要嘛你避开这个问题,要嘛继续追问...并期待有个热心人士会为你解答。

很多年以后你会意识到,这世上没有那么多热心人士,你的疑问任然是疑问。

很多年以后你发现,你需要避开的问题越来越多,最后连 Angular 你都避开了,逃到了 Vue,React,但终究没有逃出问题的魔掌。

最后你意识到原来问题与框架无关,问题来自于项目的复杂度和你掌握知识的深度。

结论:直面问题,问题解决 33%,理解问题,问题解决 +33%,最后的 33% 就靠你的智慧了,而这 3 步都离不开知识。

当 ngForTemplate 遇上 Query Elements (疑难杂症)

上述的例子不够简单,这里我做一个更直观的例子来凸显同一个原因导致的问题。

NgForOf 指令 和 Query Elements

App 组件

export class AppComponent {
names = signal(["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Jane"]); trackByNameFn(_index: number, name: string): string {
return name;
}
}

一组名字和一个 trackByNameFn 方法准备给 NgForOf 指令。

App Template

<h1 *ngFor="let name of names(); trackBy: trackByNameFn">{{ name }}</h1>

注:问题的原因并不出在 NgForOf 指令,它只是作为例子而已,请耐心往下看。

使用 NgForOf 指令 for loop 出所有名字。

效果

接着我们给 h1 添加一个 Template Variable

然后在 App 组件添加 Query Elements

export class AppComponent {

  // 1. Query Elements
h1List = viewChildren<string, ElementRef<HTMLElement>>('h1', { read: ElementRef });
constructor(){
afterNextRender(() => { // 2. Log Query Results
console.log(this.h1List().map(el => el.nativeElement.textContent));
});
}
}

效果

目前为止一切正常,接下来,我们换个位置。

在 App Template 添加一个 change sort button

<button (click)="changeSort()">Change Sort</button>

在 App 组件添加 changeSort 方法

changeSort() {
this.names.set([
...this.names().slice(1, 5),
this.names()[0],
...this.names().slice(5),
]) setTimeout(() => {
console.log('after', this.h1List().map(el => el.nativeElement.textContent));
}, 1000);
}

换位置后,我们查看 Query Results 的顺序是否也跟着换了位置。

效果

完全正确。

ngForTemplate 和 Query Elements

现在,我们做一些调整,改用 ngForTemplate。

<ng-template #template let-name>
<h1 #h1>{{ name }}</h1>
</ng-template> <ng-template ngFor [ngForOf]="names()" [ngForTrackBy]="trackByNameFn" [ngForTemplate]="template"></ng-template>

我在 NgForOf 指令教程中没有提到 @Input ngForTemplate 是因为它比较冷门,而且可能会遇到本篇的问题。

ngForTemplate 允许我们把 ng-template 定义在另一个地方,然后传入到 NgForOf 指令里。

这样的好处是 ng-template 可以另外封装,更 dynamic,更灵活,当然也更容易掉坑。

接着,我们做回相同的测试

注意看,DOM render 是正确的,但是 Query Results 的顺序是完全错误的。

Smallest reproduction

首先,不要误会,这不是 NgForOf 指令的问题,更不是 @Input ngForTemplate 的问题。

这是 ng-template 和 ViewContainerRef 的问题。

如果你已经忘记了 ng-template 和 ViewContainerRef 的原理,你可以先复习这篇 Component 组件 の ng-template

我们用 ng-template 和 ViewContainerRef 来重现上述的问题。

首先在 App Template 添加 ng-container

<button (click)="changeSort()">Change Sort</button>

<ng-template #template let-name>
<h1 #h1>{{ name }}</h1>
</ng-template> <!-- 1. 加上 ng-container -->
<ng-container #container />

注:NgForOf 指令可以删除了。

接着在 App 组件 Query TemplateRef 和 ViewContainerRef,然后 for loop createEmbeddedView 输出所有名字。

export class AppComponent implements OnInit {
viewContainerRef = viewChild.required('container', { read: ViewContainerRef });
templateRef = viewChild.required('template', { read: TemplateRef }); ngOnInit() {
for (const name of ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Jane"]) {
this.viewContainerRef().createEmbeddedView(this.templateRef(), { $implicit: name })
}
} h1List = viewChildren<string, ElementRef<HTMLElement>>('h1', { read: ElementRef }); trackByNameFn(_index: number, name: string): string {
return name;
}
}

接着实现 changeSort 方法

changeSort() {
this.viewContainerRef().move(this.viewContainerRef().get(0)!, 4); setTimeout(() => {
console.log('after', this.h1List().map(el => el.nativeElement.textContent));
}, 1000);
}

通过 ViewContainerRef.move 换位置

效果

注意看,DOM render 是正确的,但是 Query Results 的顺序是错误的。它和 ngForTemplate 都出现了顺序错误的问题。

The reason behind

我们逛过 Angular 源码,所以我们知道:

ng-template 会生成 LContainer (type = 4 号 Container)。

ng-container + Query ViewContainerRef 也会生成 LContainer (type = 8 号 ElementContainer)。

ViewContainerRef.createEmbededView 会生成一个 LView,

这个 LView 会被记入到 2 个地方:

  1. ng-container LContainer

    LVIew 会被记入到 ng-container LContainer[8 ViewRefs] 的 array 里,和 LContainer[10 以上],这两个始终是一致的啦,我们下面关注 LContainer[8 ViewRefs] 就好。

  2. ng-template LContainer

    LVIew 会被记入到 ng-template LContainer[9 MovedViews] 里头

    index 9 装的是 Moved Views,意思是说,用这个 ng-template 创建出来的 LView 却没有被插入到这个 ng-template 的 LContainer 里,而是被插入到了其它的 LContainer。

好,重点来了。

第一个重点,此时 ng-template LContainer[9 MovedViews] 和 ng-container LContainer[8 ViewRefs] 的 LView array 顺序是一模一样的。

第二个重点,Query Elements 查找的是 ng-template LContainer[9 MovedViews] 里头的 LView,所以 Query Results 的顺序是依据 ng-template LContainer[9 MovedViews] array 的顺序。

接着,我们 change sort 看看

ng-template LContainer

ng-container LContainer

DOM render 是依据 ng-container LContainer[8 ViewRefs],Query 则是依据 ng-template LContainer[9 MovedViews],而这 2 个 array 的顺序在 change sort 以后竟然不一样了。

好,原因算是找到了。

逛一逛 ViewContainerRef.move 源码

我们知道 ViewContainerRef.move 后,ng-container LContainer[8 ViewRefs] 和 ng-template LContainer[9 MovedViews] 的 array 顺序就不同了,但具体是哪一行代码导致的呢?

ViewContainerRef 源码在 view_container_ref.ts

ViewContainerRef.move 方法内部其实是调用了 insert 方法。

insert 内调用了 insertImpl

首先检查看要 insert 的 LView 是否已经在 LContainer 里,如果已经在,那就先 detach。

提醒:只是 detach,没有 destroy 哦。

detach 以后,ng-container LContainer[8 ViewRefs] 就少了这个 LView,同时 ng-template LContainer[9 MovedViews] 也少了这个 LView

然后再重新插入回 LContainer。注:顺便留意那个要插入的 index 位置。

addLViewToLContainer 函数的源码在 view_manipulation.ts

insertView 函数的源码在 node_manipulation.ts

到这里,ng-container LContainer[10 以上] 就有正确的 LView 了。

继续往下

我们的例子就是 LView 来自其它地方。

ng-template 本身也可以作为 ViewContainerRef。

ng-template create LView 插入回自己作为 ViewContainerRef 叫做来自同一个地方。

ng-template create LView 插入到其它的 LContainer 叫做来自不同地方。

来自不同地方就需要调用 trackMovedView 函数

到这里,ng-template LContainer[9 MovedViews] 就有了 LView,但是顺序和 ng-container[10 以上] 是不同的。

因为它只是 push,完全没有依据 insert 指定的 index。

好,我们直接跳回 ViewContainerRef.insertImpl 方法

在 addLViewToLContainer 后,会跑一个 addToArray。它的作用是把 LView 添加到 LContainer[8 ViewRefs] array 里面。

它是有依据 insert 指定的 index 的。

总结:

  1. 先 detach LView

    ng-container LContainer 和 ng-template LContainer 都移除这个 LView

  2. 添加 LView 到 ng-container LContainer[10 以上]

    这里会依据 insert 指定的 index

  3. 添加 LView 到 ng-template LContainer[9 MovedViews]

    这里不会依据 insert 指定的 index,它一律只是 push。

    这也是这个问题出错的地方。

  4. 添加 LView 到 ng-container LContainer[8 ViewRefs]

    这里会依据 insert 指定的 index

对这个问题的思考

目前的行为是:ng-template 创建的 LView 被插入到 ng-container LContainer 后,ng-template LContainer[9 MovedViews] 只是一味的 push,没有顾虑到顺序。

要维护一个顺序其实也不难,只是我们也要考虑到 ng-template 的 LView 是可以插入到不同的 LContainer 的。

试想 ng-template 创建了 9 个 LView,分别插入到 3 个不同的 ng-container LContainer 里。

LView 的顺序可以 follow 个别的 ng-container LContainer[8 ViewRefs],但是 3 个 ng-container LContainer 的顺序呢?哪一个先?

这个就需要思考一下,最简单的选择或许是依据插入的顺序。

总之这样至少已经可以解决 80% 常见了,毕竟 ng-template 插入到多个 LContainer 是罕见的。

再补一个例子

用 hacking way 实现 Signal-based OnInit。

请移步:Angular 高级教程 – Signals # Signal-based OnInit? (the hacking way...)

总结

本篇给了一个例子,示范当面对疑难杂症时如何面对,如何理解,如何一步一步思考,并且选出最合适的方案。

同时,让你对知识的力量有所感悟,以后你就知道什么时候需要深入学习,什么后该划水。happy coding...

目录

上一篇 Angular 18+ 高级教程 – Prettier, ESLint, Stylelint

下一篇 Angular 18+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding

Angular 18+ 高级教程 – 学以致用的更多相关文章

  1. Siki_Unity_2-9_C#高级教程(未完)

    Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...

  2. Pandas之:Pandas高级教程以铁达尼号真实数据为例

    Pandas之:Pandas高级教程以铁达尼号真实数据为例 目录 简介 读写文件 DF的选择 选择列数据 选择行数据 同时选择行和列 使用plots作图 使用现有的列创建新的列 进行统计 DF重组 简 ...

  3. ios cocopods 安装使用及高级教程

    CocoaPods简介 每种语言发展到一个阶段,就会出现相应的依赖管理工具,例如Java语言的Maven,nodejs的npm.随着iOS开发者的增多,业界也出现了为iOS程序提供依赖管理的工具,它的 ...

  4. 【读书笔记】.Net并行编程高级教程(二)-- 任务并行

    前面一篇提到例子都是数据并行,但这并不是并行化的唯一形式,在.Net4之前,必须要创建多个线程或者线程池来利用多核技术.现在只需要使用新的Task实例就可以通过更简单的代码解决命令式任务并行问题. 1 ...

  5. 【读书笔记】.Net并行编程高级教程--Parallel

    一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机 ...

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

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

  7. 展讯NAND Flash高级教程【转】

    转自:http://wenku.baidu.com/view/d236e6727fd5360cba1adb9e.html 展讯NAND Flash高级教程

  8. Net并行编程高级教程--Parallel

    Net并行编程高级教程--Parallel 一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控 ...

  9. [转帖]tar高级教程:增量备份、定时备份、网络备份

    tar高级教程:增量备份.定时备份.网络备份 作者: lesca 分类: Tutorials, Ubuntu 发布时间: 2012-03-01 11:42 ė浏览 27,065 次 61条评论 一.概 ...

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

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

随机推荐

  1. Day 9 - 线段树

    线段树 引入 线段树是算法竞赛中常用的用来维护 区间信息 的数据结构. 线段树可以在 \(O(\log N)\) 的时间复杂度内实现单点修改.区间修改.区间查询(区间求和,求区间最大值,求区间最小值) ...

  2. C# 枚举帮助类EnumHelper(获取描述、名称和数值)

    帮助类定义 public class EnumHelper { #region 静态方法 public static Dictionary<string, string> GetEnumD ...

  3. 关于elementUI的select组件回显问题

    最近接受了一个后台项目,需求是这样的,点击表单项,弹出的弹出层显示该表单项目的信息.但是回显的时候,关于弹出层中的级联显示有问题,如图: 回显结果为: 回显代码为: 弹框为: 我就不明白了,分明分公司 ...

  4. 如何用 WinDbg 调试Linux上的 .NET程序

    一:背景 1. 讲故事 最新版本 1.2402.24001.0 的WinDbg真的让人很兴奋,可以将自己伪装成 GDB 来和远程的 GDBServer 打通来实现对 Linux 上 .NET程序进行调 ...

  5. 【工具】SpringBoot项目如何查看某个maven依赖是否存在以及依赖链路

    当我在SpringBoot项目中想加个依赖,但是不确定现有依赖的依赖的依赖.....有没有添加过这个依赖,怎么办呢?如果添加过了但是不知道我需要的这个依赖属于哪个依赖的下面,怎么查呢? IDEA中提供 ...

  6. PKUWC2024游记

    PKUWC2024 游记 day -???? 得知今年冬令营在育才,非常高兴不用出远门了. day 1 当天上午 7:00 起来,然后做车去报道,非常堵车.感觉育才环境挺好的,~不像某人在读学校一样. ...

  7. 【Spring】03 XML配置

    Alias别名设置 可以为一个Bean的ID再设置一个ID 多一个可用标识,大概... 在获取实例注入参数时,两个标识都可以使用 除了Alias可以设置别名之外,Bean的标签本身也可以设置第二别名 ...

  8. 【Vue】Vue-Cli 安装

    首先需要Node.js环境支持: Node.js官网下载: https://nodejs.org/en/ 右边稳定版,左边最新版 下载安装程序之后双击运行,无脑下一步 打开终端输入版本查看命令: no ...

  9. 【Vue】10 Vue-Cli 项目创建

    简单的Demo案例并不需要Vue-Cli,因为一个页面之内可以总揽 但是真实的项目开发,考虑代码结构,目录结构,部署,热部署,单元测试... 代码量呈几何倍数增长,而且缺少轮子就写起来很痛苦 所以必须 ...

  10. 家庭局域网中电脑唤醒 —— WOL远程唤醒(python实现)

    相关: https://blog.csdn.net/hih30250/article/details/136342258 在WOL介绍里说过WOL数据包的最简格式是由6个字节的255和目标计算机的48 ...