前言

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

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

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

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

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

下面是我在 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. centos下安装Docker容器

    安装前的准备工作 1.列出docker安装过的相关包 sudo yum list installed | grep docker 2.删除相关安装包 #根据查找出来的进行删除,不同版本可能有不一样的情 ...

  2. Javascript克隆数据

    JS 复制数据 1 浅复制 具体方法 // 数组 Array.prototype.slice // 普通对象 Object.assign 思考 2 深复制 1) function deepClone( ...

  3. OpenFileDialog的使用实例

    'Dim excelFolderPath As String = "" 'Dim openFileDialog1 As New OpenFileDialog() 'openFile ...

  4. 写写Redis十大类型stream的常用命令

    前言:感觉这个好像没啥用,我没学过mq,好像这个东西用别的中间件比较好,比如kafka,rabbitmq 最后一个是bitfield,看尚硅谷介绍说那玩意基本用不着,所以那个也不看了,直接跳了,十大类 ...

  5. vue加载三维模型

    创建项目 我使用的是Vue CLI3,具体创建不再赘述,网上教程很多 下载SuperMap iClient3D for WebGL产品包 链接:http://support.supermap.com. ...

  6. 【RabbitMQ】05 通配符模式

    需要设定交换机模式为通配符模式 Topic 在绑定规则上采用通配描述实现动态绑定 创建通配符模式的生产者 package cn.dzz.topicQueue; import com.rabbitmq. ...

  7. 【Project】原生JavaWeb工程 01 概述,搭建

    一.环境准备: 操作系统:Windows7 或者 Windows10 IDE集成环境:IDEA 2018版本或者更高 数据库:MySQL 5版本或者更高 服务器:Tomcat 8版本或者更高 二.数据 ...

  8. 【Vue】Re19 Promise

    一.概述 Promise是异步编程的解决方案 异步事件的处理: 封装的异步请求函数不能立即获取结果, 通常会传入另外一个函数,在请求成功的时候将数据通过传入的函数回调出去 如果只是一个简单的请求,那么 ...

  9. Audio2Gesture:NVIDIA 黄仁勋的3D虚拟人技术 —— 元宇宙

    相关: https://www.nvidia.com/en-us/on-demand/session/omniverse2020-om1573/ https://www.nvidia.com/zh-t ...

  10. 【转载】Typora 的 Markdown 语法

    原文地址: Typora 的 Markdown 语法 详细见原文: https://support.typoraio.cn/zh/Markdown-Reference/