0%

Introduction

Angular里面的Injector是分层级的,主要有以下两个层级:

  • EnvironmentInjector
  • ElementInjector

EnvironmentInjector

EnvironmentInjector层级是通过以下两种方式创建的:

  1. @Injectable({ providedIn: 'xxx' }) - xxx 可以是 root, platform.
  2. providers array in ApplicationConfig

ElementInjector

ElementInjector是创建在每个Dom元素上的,初始时是空的,当我们在@Directive或者@Component中声明了一个provider时,Angular会在ElementInjector中创建一个实例。

ModuleInjector

在基于模块的应用中,每个模块都有一个ModuleInjector,ModuleInjector可以通过如下方式配置:

  1. Injectable({ providedIn: 'xxx' }) - xxx 可以是 platform, root.
  2. providers array in @NgModule

PlatformInjector

rootInjector之上,还有两个Injector,分别是:

  1. 一个额外的EnvironmentInjector - 即PlatformInjector.
  2. NullInjector.

注意:PlatformInjector 是为了多个Angular app服务的,对于基于Nx的超大型Angular项目,一个项目中可能包含多个app(比如使用了Module Federation)而root injector是针对app的,如果要跨app共享服务,那么就要使用PlatformInjector了。

看一下Angular的启动代码(位于src/main.ts文件中):

1
bootstrapApplication(AppComponent, appConfig);

当执行上述代码时,Angular首先创建一个Platform Injector(也是Environment Injector),然后创建一个Platform Injector的child Injector,也就是root Environment Injector。而在Platform Injector之上,还有一个额外的NullInjector。

所以NullInjector是整个Injector链条的顶端,我们在查找一个依赖时,会顺着这个链条向上查找,如果到了NullInjector还没找到,则会抛出异常。相信很多同学都遇到过下面的错误吧,仔细看报错信息第一行,就是NullInjector!
NullInjector

ElementInjector

Angular会为每个Dom元素隐式创建一个ElementInjector,当我们在@Directive或者@Component中声明了一个provider或者viewProvider时,Angular会在ElementInjector中创建一个实例。

依赖解决规则(查找规则)

当Angular为一个Component或者Directive查找依赖时,遵循如下规则:

  1. 先查找Component或者Directive自身的ElementInjector,如果找到则返回。否则进入下一步。
  2. 查找Component或者Directive的Parents的ElementInjector,再查找Parents的Parents的ElementInjector,一直如此直到找到,否则进入下一步。
  3. 查找Component或Directive的EnvironmentInjector层级,此过程同ElementInjector,一直向上查找,如果找到则返回,否则报错。

此处应该加入一张图。

依赖解决修饰符

上面介绍的依赖解决规则,可以用以下修饰符来改变,你可以从@angular/core中引入这些修饰符:

  1. @Optional - 如果找不到依赖,不会报错,而是返回null
  2. @SkipSelf - 跳过当前ElementInjector,向上查找。
  3. @Self - 只在当前ElementInjector中查找依赖,不会向上查找。
  4. @Host - 只在当前Component及其Host Element的Injector中查找依赖。

这些修饰符可以分为三类:

  1. What to do if Angular doesn’t find what you’re looking for, this is @Optional()
  2. Where to start looking, that is @SkipSelf.
  3. Where to stop looking, @Host and @Self.

使用修饰符

这些修饰符都是在constructor中注入依赖时使用的,比如:

1
constructor(@Optional() private myService: MyService) {}

关于这些修饰符的详细使用规则,请看这篇

什么是Observable

Observable是RxJS中的一个类,它是一个可观察对象,用于表示一个异步数据流。Observable可以发出三种类型的值:nexterrorcompletenext表示发出一个值,error表示发出一个错误,complete表示数据流结束。

Cold Observable

Cold Observable有以下特点:

  • 一般来说是单播的(Unicast),每个订阅者都会有自己的数据流。
  • 一般是惰性的,创建时并不发出数据,只有有订阅时才会开始发出数据。

Angular中的Http请求就是一个Cold Observable,每次订阅都会发起一个新的请求。

Hot Observable

Host Observable一般用Subject来实现,Subject是一种特殊的Observable,它既是Observable也是Observer。

Hot Observable有以下特点:

  • 一般来说是多播的(Multicast),所有订阅者共享同一个数据流。
  • 一般是热的,创建后就开始发出数据,即使没有订阅者也发出数据。

什么是背压

Back pressure 翻译成中文叫做背压,也有叫回压的,那么到底啥是背压呢?望文思义一下,背压就是来自背后的压力,那么背后是指哪里呢?在RxJS中,背后就是指产生数据的Observable,我们姑且称之为生产者,而订阅这个Observable的Observer就是消费者。背压就是生产者生产数据的速度大于消费者消费的速度,导致大量的数据积压。

现实生活中的例子,你老婆一分钟能烙五个饼,而你每分钟只能吃一个,那么烙好的饼就会越积越多,你就吃不消了。

解决背压

解决背压基本就两种方法:缓冲和丢弃,我们在时髦一下,在泛化一下,就是无损处理和有损处理。

  • Lossy Operation
  • Lossless Operation

有损方式

  • debounce
  • throttle

无损方式

  • bufferCount - 按数量缓冲,攒够Count个数据再发出
  • bufferTime - 按时间缓冲,攒够Time时间内的数据再发出
  • buffer - 按条件缓冲,满足条件时发出缓冲的数据, buffer(xxx), xxx是一个Observable,当xxx发出数据时,发出缓冲的数据。

Introduction

Angular中有三种样式封装方式,分别是:Emulated, ShadowDom, and None

  • ViewEncapsulation.Emulated (Default value)
  • ViewEncapsulation.ShadowDom
  • ViewEncapsulation.None

可以用过Component中的encapsulation属性来指定封装方式,如果不指定的话,默认是Emulated

1
2
3
4
5
6
7
8
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.scss'],
encapsulation: ViewEncapsulation.ShadowDom, // <--- here
})
export class ProductComponent {
}

下面我们分别讲解三种封装方式。

Emulated

这是默认的封装方式,如果你没有在@Component中显式指定encapsulation的话,那么使用的就是该方式。在该方式下,Angular会生成一个随机字符串做为组件的属性,然后将样式应用到这个属性上。
这种封装方式是模拟Shadow DOM的方式,Angular会更改组件的CSS选择器,以便样式只应用于组件的视图。这意味着组件的样式不会影响应用程序的其他部分。

以下面的Product组件为例,product-home.component.scss这个样式文件里面的样式只会影响ProductHomeComponent这个组件对应的视图.

1
2
3
4
5
6
7
8
@Component({
selector: 'app-product-home',
templateUrl: './product-home.component.html',
styleUrl: './product-home.component.less',
})
export class ProductHomeComponent {
protected readonly name = 'Philip';
}

运行app后,我们可以在浏览器中inspect生成后的页面,可以看到app-product-home元素有一个唯一的属性_nghost-ng-c3352351300:

1
2
3
<app-product-home _nghost-ng-c3352351300>
<!-- ... -->
</app-product-home>

再看一下实际的例子,假如我们有如下html template, ProductHomeComponent中引入了ProductDetailComponent

1
2
3
<!-- product-home.component.html -->
<p>product-home works!</p>
<app-product-detail></app-product-detail>

那么生成的HTML如下:
emulated

可以看到,Angular为生成的Dom节点都添加了额外的属性,这些属性大概分为两类:

  • _nghost-xxxx - 用来标记组件的host element,比如上图中第一个红框内
  • _ngcontent-xxxx - 用来标记组件的子元素,比如上图中app-product-home内的p元素,它就是一个纯内容,所以标记是_ngcontent-xxxx
  • 有些元素既是host, 又是content,比如上图的第二个红框。因为ProductDetailComponent既是ProductHomeComponent的子元素,而它本身又是一个组件。所以它同时拥有_nghost-xxxx_ngcontent-xxxx两个属性。

如果我们给上面的p标签添加一个背景色的话,那么观察生成后的css样式,你会发现,样式文件里面也添加了额外的属性,这样就确保了样式的唯一性。

观察下图中左侧p标签的属性,和右侧css文件中product-container的属性,可以看到他们是一一对应的。
emulated-css

Emulated模式生成的样式最终会插入到html文件的head标签中。(可以运行Angular app,然后鼠标右键-inspect查看对应的html文件代码)

ShadowDom

这种方式使用浏览器内置的Shadow DOM API来封装组件的样式,该模式下,组件被放到一个ShadowRoot之下,这个Shadow Root相当于组件的host element, 这意味着组件的样式不会影响应用程序的其他部分。

该模式生成的样式会直接放到shadow-root中。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product-home',
templateUrl: './product-home.component.html',
styleUrl: './product-home.component.less',
encapsulation: ViewEncapsulation.ShadowDom,
})
export class ProductHomeComponent {
protected readonly name = 'Philip';
}

注意观察生成的html文件,app-product-home元素会有一个shadow-root:

1
2
3
4
<app-product-home>
#shadow-root (open)
<!-- ... -->
</app-product-home>

None

这种模式不进行任何封装,相当于裸奔,其效果等于直接在html中引入样式文件。这种模式谨慎使用,因为会有样式污染。

该模式生成的样式会直接放到html文件的head中。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product-home',
templateUrl: './product-home.component.html',
styleUrl: './product-home.component.less',
encapsulation: ViewEncapsulation.None,
})
export class ProductHomeComponent {
protected readonly name = 'Philip';
}

The generated HTML will not have any unique attributes or shadow root:

1
2
3
<app-product-home>
<!-- ... -->
</app-product-home>

注意:ViewEncapsulation.None ViewEncapsulation.Emulated中的样式同时会插入到ViewEncapsulation.ShadowDom的组件中。

References

  1. https://v17.angular.io/guide/view-encapsulation - 这里面的例子要仔细研究一下,有很多细节。

Introduction

ngSwitch is a structural directive in Angular, it is used to conditionally render a template based on the value of an expression.

Use ngSwitch with enum.

注意:我发现下面每个*ngSwitchCase分支都只包含一个<p>元素,所以我们可以将ng-container拿掉,直接把*ngSwitchCase放在<p>标签上。
但是如果case分支中有多个元素,那么就需要用ng-container包裹起来。

default分支用*ngSwitchDefault来标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--app.component.html-->
<div class="account-feature-container" >
<ng-container [ngSwitch]="accountStatus">
<ng-container *ngSwitchCase="AccountStatus.CLOSED">
<p>This is a closed account</p>
</ng-container>
<ng-container *ngSwitchCase="AccountStatus.AUTHORIZED">
<p>This is an authorized account</p>
</ng-container>
<ng-container *ngSwitchCase="AccountStatus.NORMAL">
<p>This is an normal account</p>
</ng-container>
<p *ngSwitchDefault>This is the default branch</p>
</ng-container>
</div>

在Component中定义一个枚举类型,注意AccountStatus = AccountStatus;这一行是必须的,否则编译报错。

1
2
3
4
5
6
7
8
9
10
11
12
// app.component.ts
@Component({
selector: 'app-ng-switch',
templateUrl: './ng-switch.component.html',
styleUrls: ['./ng-switch.component.less']
})
export class NgSwitchComponent implements OnInit {
AccountStatus = AccountStatus; // Make enum visible in component.ts file.
public accountStatus: AccountStatus = AccountStatus.AUTHORIZED;
constructor() {}
ngOnInit(): void {}
}

Use built-in structural directive @switch.

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="use_built-in_control_flow">
@switch (accountStatus) {
@case (AccountStatus.CLOSED) {
<p>This is a closed account</p>
} @case (AccountStatus.AUTHORIZED) {
<p>This is an authorized account</p>
} @case (AccountStatus.NORMAL) {
<p>This is an normal account</p>
} @default {
<p>This is the default branch</p>
}
}
</div>

Tips:

  1. Remember to add the @default branch at the end, if no case matched, and no @default is provided, nothing will be rendered.
  2. There is no fallthrough in the @switch directive, so you don’t need to add break or return in each @case branch.

Introduction

ngIf is a structural directive in Angular, it is used to conditionally render content in a template. Remember to import CommonModule in the module where you want to use ngIf.

Full syntax with ng-template

Structural directives are used in ng-template, here is an example:

1
2
3
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}</div>
</ng-template>

Shorthand syntax

But, in practice, we often use ngIf in a more concise way by prefix a * before it, this is called the shorthand syntax:

1
<div *ngIf="hero" class="name">{{hero.name}}</div>

Note, even we write it in the shorthand syntax, Angular will still convert it to the ng-template syntax after compiling.

The above template will create a div element in the template, What if you don’t want to create a div element? You can use ng-container instead of div:

1
2
3
<ng-container *ngIf="condition">
Content to render when condition is true
</ng-container>

In this way, Angular will render the text directly without creating a div element.

New syntax with @if

With the new built-in structural @if directive, you can use the following syntax:

1
2
3
@if (condition) {
Content to render when condition is true
}

If else then with *ngIf

1
2
3
4
5
6
7
8
9
10
<div class="case-3">
<ng-container *ngIf="condition; then thenBlock else elseBlock">
</ng-container>
<ng-template #thenBlock>
Content to render when condition is true
</ng-template>
<ng-template #elseBlock>
Content to render when condition is false
</ng-template>
</div>

If else then with @if

With @if directive, you can use the following syntax, it’s more simple and readable:

1
2
3
4
5
6
7
<div class="case-3">
@if (condition) {
Content to render when condition is true
} else {
Content to render when condition is false
}
</div>

Conclusion

既然Angular已经有ngIf, ngFor, ngSwitch等内置的结构性指令,为什么还要引入新的@if@for, @switch呢?主要有以下原因

  1. 更简洁的语法,比如要实现if - else if - … - else的逻辑,使用ngIf就会显得很臃肿。而使用@if就可以很好的解决这个问题。
  2. 更好的类型检查机制,这个不知道是怎么实现的。
  3. 更好的运行时性能,Angular编译器会将@if@for@switch编译成更高效的代码,这样可以减少运行时的开销。
  4. 更小的包体积,最多可以减少30kb的包体积。
  5. 不需要额外导入CommonModule模块,直接使用即可,ngIf等指令则需要导入CommonModule模块。

RxJS Use Case

统计页面点击次数

注意,RxJS中的scan相当于JavaScript中的Array.prototype.reduce,下面的例子展示了如何统计页面点击次数。

1
2
3
4
const { fromEvent, scan } = rxjs;
fromEvent(document, "click")
.pipe(scan((count) => count + 1, 0))
.subscribe((count) => console.log(count));

节流

Resize窗口

resize时间触发非常频繁,我们可以使用throttleTime来节流,下面的例子展示了如何节流resize事件。

1
2
3
4
5
6
7
const { fromEvent, throttleTime } = rxjs;
fromEvent(window, "resize")
.pipe(throttleTime(500))
.subscribe(() => {
console.log(`window width: ${window.innerWidth}`);
console.log(`window height: ${window.innerHeight}`);
});

统计点击次数

throttleTime可以用来实现节流,下面的例子展示了如何实现节流,无论用户点击的有多快,我们都是每隔1秒统计一次点击次数。

1
2
3
4
5
6
7
const { fromEvent, throttleTime, scan } = rxjs;
fromEvent(document, "click")
.pipe(
throttleTime(1000),
scan((count) => count + 1, 0)
)
.subscribe((count) => console.log(count));

设置最大并发请求数

mergeMap with concurrency参数可以设置最大并发请求数,下面的例子展示了如何设置最大并发请求数。

1
2
3
4
import { interval, mergeMap, of } from 'rxjs';

const source = interval(1000);
source.pipe(mergeMap(() => of('request'), 2)).subscribe(console.log);

避免重复请求

使用switchMap可以避免重复请求,下面的例子展示了如何避免重复请求。

忽略某个值

使用skip, 以下代码忽略了interval的前两个值,然后取3个值。所以忽略0和1,取2, 3, 4。关于interval的详细信息,请看这里

1
2
3
import { interval, skip, take } from 'rxjs';

interval(1000).pipe(skip(2), take(3)).subscribe(console.log); // 2, 3, 4

Introduction

RxJS中有许多创建Observable的方法,如fromofinterval等,本文一一介绍。

from

from方法可以将一个数组、类数组对象、Promise、可迭代对象、Observable-like对象转换为Observable。

将数组转换为Observable

RxJS社区有一个约定,以$结尾的变量代表一个Observable对象。下面的例子将一个数组转换为Observable。

1
2
3
4
5
import { from } from 'rxjs';

const array = [1, 2, 3];
const array$ = from(array);
array$.subscribe(console.log); // 1, 2, 3

上面的代码可以简化为:

1
2
3
import { from } from 'rxjs';

from([1, 2, 3]).subscribe(console.log); // 1, 2, 3

将Promise转换为Observable

下面的例子将一个Promise对象转换为Observable。

1
2
3
4
5
6
const p = new Promise((resolve) => {
setTimeout(() => {
resolve([1, 2, 3]);
}, 1000);
});
from(p).subscribe(console.log);

将可迭代对象转换为Observable

其实数组就是可迭代对象,所以前面数组的例子已经包含该例子,下面的例子将一个可迭代对象转换为Observable。

1
2
3
4
5
6
7
function* generate123() {
yield 1;
yield 2;
yield 3;
}

from(generate123()).subscribe(console.log);

使用asapScheduler

使用asapScheduler可以让from方法在当前任务队列的末尾执行,下面的例子展示了如何使用asapScheduler。你知道以下代码输出什么吗?如果不加asapScheduler,输出会是什么?

1
2
3
console.log('from start...');
from([1, 2, 3], asapScheduler).subscribe(console.log);
console.log('from end...');

以上代码输出如下:

1
2
3
4
5
from start...
from end...
1
2
3

如果去掉asapScheduler,则from同步执行,输出如下:

1
2
3
4
5
from start...
1
2
3
from end...

from的以下书写方式已经被废弃,将在RxJS 8中移除.

1
from([1, 2, 3], asapScheduler).subscribe(console.log);

推荐使用下面的方式,详情看这里

1
scheduled([1, 2, 3], asapScheduler).subscribe(console.log);

将Dom事件转换为Observable

使用fromEvent方法可以将Dom事件转换为Observable。
以下代码监听documentclick事件,当点击document时,会在控制台输出clicked!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
<title>rxjs demo</title>
</head>
<body>
<script>
const { fromEvent } = rxjs;
fromEvent(document, "click").subscribe(() => {
console.log("clicked!");
});
</script>
</body>
</html>

默认情况下,浏览器的事件模型是冒泡模型,也可以改为捕获模型,只需要传递{ capture: true }作为fromEvent的第三个参数即可。

1
2
3
fromEvent(document, "click", { capture: true }).subscribe(() => {
console.log("clicked!");
});

of

of把输入的值转换为Observable。与from不同的是,of不会做任何flatten操作,它会把输入的值当做一个整体。这一点体现在数组上,from会把数组的每个元素当做一个值,而of会把整个数组当做一个值。

1
2
3
import { of, from } from 'rxjs';
of([1, 2, 3]).subscribe(console.log); // [1, 2, 3]
from([1, 2, 3]).subscribe(console.log); // 1, 2, 3

of通常用在要一次性生成多个值的场景,比如下面的例子:

1
2
import { of } from 'rxjs';
of(1, 2, 3).subscribe(console.log); // 1, 2, 3

range

Emit a sequence of numbers in a range.

在一个范围内生成一个Observable。range方法有三个参数,分别是startcountschedulerstart表示起始值,count表示生成的个数,scheduler表示调度器。

1
2
3
import { range } from 'rxjs';

range(1, 3).subscribe(console.log); // 1, 2, 3

interval

interval方法会每隔一段时间发出一个递增的数字。interval方法有一个参数period,表示时间间隔,单位是毫秒。interval默认使用asyncScheduler

1
2
3
import { interval } from 'rxjs';

interval(1000).subscribe(console.log); // 0, 1, 2, 3, 4, ...

有三点需要注意:

  1. interval方法是异步的,默认使用asyncScheduler
  2. interval方法会从0开始发出数字。
  3. interval方法第不是立即执行的,而是在第一个时间间隔之后执行。

以下代码输出什么?

1
2
3
console.log('interval start...');
interval(1000).subscribe(console.log);
console.log('interval end...');

答:因为interval默认使用asyncScheduler,所以interval方法是异步的,所以interval start...interval end...会先输出,然后再输出0, 1, 2, 3, 4, ...

timer

可以将timer看成是高配版的interval,为啥这么说呢?因为interval只能设置时间间隔,却无法控制开始时间(interval的开始时间就是其时间间隔),而timer既能控制间隔时间,也能控制开始时间。

来看这样一个需求,每个1秒产生一个数字(从0开始),但是要求第一个数字立即产生,而不是等待1秒。interval无法满足这个需求,但是timer可以。

1
2
3
import { timer } from 'rxjs';

timer(0, 1000).subscribe(console.log); // 0, 1, 2, 3, 4, ...

timer方法的第一个参数就是开始时间,除了可以指定一个具体的毫秒数之外,还可以指定一个Date对象,表示在指定的时间开始。

1
timer(new Date(2024, 11, 28, 17, 0, 0), 1000).subscribe(console.log); // 0, 1, 2, 3, 4, ...