Introduction
我今天看Angular官方文档才知道,原来Angular里面竟然还有一个viewProvider
,这个是什么东东呢?我们一起来看看。
Angular里面的Injector是分层级的,主要有以下两个层级:
EnvironmentInjector层级是通过以下两种方式创建的:
@Injectable({ providedIn: 'xxx' })
- xxx 可以是 root
, platform
.providers
array in ApplicationConfig
ElementInjector是创建在每个Dom元素上的,初始时是空的,当我们在@Directive
或者@Component
中声明了一个provider时,Angular会在ElementInjector中创建一个实例。
在基于模块的应用中,每个模块都有一个ModuleInjector,ModuleInjector可以通过如下方式配置:
Injectable({ providedIn: 'xxx' })
- xxx 可以是 platform
, root
.providers
array in @NgModule
在root
Injector之上,还有两个Injector,分别是:
PlatformInjector
.注意: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
!
Angular会为每个Dom元素隐式创建一个ElementInjector,当我们在@Directive
或者@Component
中声明了一个provider
或者viewProvider
时,Angular会在ElementInjector中创建一个实例。
当Angular为一个Component或者Directive查找依赖时,遵循如下规则:
此处应该加入一张图。
上面介绍的依赖解决规则,可以用以下修饰符来改变,你可以从@angular/core
中引入这些修饰符:
@Optional
- 如果找不到依赖,不会报错,而是返回null
。@SkipSelf
- 跳过当前ElementInjector,向上查找。@Self
- 只在当前ElementInjector中查找依赖,不会向上查找。@Host
- 只在当前Component及其Host Element的Injector中查找依赖。这些修饰符可以分为三类:
@Optional()
@SkipSelf
.@Host
and @Self
.这些修饰符都是在constructor
中注入依赖时使用的,比如:
1 | constructor(@Optional() private myService: MyService) {} |
关于这些修饰符的详细使用规则,请看这篇。
Observable
是RxJS中的一个类,它是一个可观察对象,用于表示一个异步数据流。Observable
可以发出三种类型的值:next
、error
和complete
。next
表示发出一个值,error
表示发出一个错误,complete
表示数据流结束。
Cold Observable有以下特点:
Angular中的Http请求就是一个Cold Observable,每次订阅都会发起一个新的请求。
Host Observable一般用Subject来实现,Subject是一种特殊的Observable,它既是Observable也是Observer。
Hot Observable有以下特点:
Back pressure
翻译成中文叫做背压
,也有叫回压
的,那么到底啥是背压呢?望文思义一下,背压就是来自背后的压力,那么背后是指哪里呢?在RxJS中,背后就是指产生数据的Observable,我们姑且称之为生产者,而订阅这个Observable的Observer就是消费者。背压就是生产者生产数据的速度大于消费者消费的速度,导致大量的数据积压。
现实生活中的例子,你老婆一分钟能烙五个饼,而你每分钟只能吃一个,那么烙好的饼就会越积越多,你就吃不消了。
解决背压基本就两种方法:缓冲和丢弃,我们在时髦一下,在泛化一下,就是无损处理和有损处理。
Angular中有三种样式封装方式,分别是:Emulated
, ShadowDom
, and None
。
可以用过Component中的encapsulation
属性来指定封装方式,如果不指定的话,默认是Emulated
。
1 | @Component({ |
下面我们分别讲解三种封装方式。
这是默认的封装方式,如果你没有在@Component
中显式指定encapsulation
的话,那么使用的就是该方式。在该方式下,Angular会生成一个随机字符串做为组件的属性,然后将样式应用到这个属性上。
这种封装方式是模拟Shadow DOM
的方式,Angular会更改组件的CSS选择器,以便样式只应用于组件的视图。这意味着组件的样式不会影响应用程序的其他部分。
以下面的Product组件为例,product-home.component.scss
这个样式文件里面的样式只会影响ProductHomeComponent
这个组件对应的视图.
1 | @Component({ |
运行app后,我们可以在浏览器中inspect生成后的页面,可以看到app-product-home
元素有一个唯一的属性_nghost-ng-c3352351300
:
1 | <app-product-home _nghost-ng-c3352351300> |
再看一下实际的例子,假如我们有如下html template, ProductHomeComponent中引入了ProductDetailComponent
1 | <!-- product-home.component.html --> |
那么生成的HTML如下:
可以看到,Angular为生成的Dom节点都添加了额外的属性,这些属性大概分为两类:
_nghost-xxxx
- 用来标记组件的host element,比如上图中第一个红框内_ngcontent-xxxx
- 用来标记组件的子元素,比如上图中app-product-home
内的p
元素,它就是一个纯内容,所以标记是_ngcontent-xxxx
。ProductDetailComponent
既是ProductHomeComponent
的子元素,而它本身又是一个组件。所以它同时拥有_nghost-xxxx
和_ngcontent-xxxx
两个属性。如果我们给上面的p
标签添加一个背景色的话,那么观察生成后的css样式,你会发现,样式文件里面也添加了额外的属性,这样就确保了样式的唯一性。
观察下图中左侧p
标签的属性,和右侧css文件中product-container
的属性,可以看到他们是一一对应的。
Emulated
模式生成的样式最终会插入到html文件的head
标签中。(可以运行Angular app,然后鼠标右键-inspect查看对应的html文件代码)
这种方式使用浏览器内置的Shadow DOM API来封装组件的样式,该模式下,组件被放到一个ShadowRoot之下,这个Shadow Root相当于组件的host element, 这意味着组件的样式不会影响应用程序的其他部分。
该模式生成的样式会直接放到shadow-root
中。
1 | @Component({ |
注意观察生成的html文件,app-product-home
元素会有一个shadow-root
:
1 | <app-product-home> |
这种模式不进行任何封装,相当于裸奔,其效果等于直接在html中引入样式文件。这种模式谨慎使用,因为会有样式污染。
该模式生成的样式会直接放到html文件的head
中。
1 | @Component({ |
The generated HTML will not have any unique attributes or shadow root:
1 | <app-product-home> |
注意:ViewEncapsulation.None
ViewEncapsulation.Emulated
中的样式同时会插入到ViewEncapsulation.ShadowDom
的组件中。
ngSwitch
is a structural directive in Angular, it is used to conditionally render a template based on the value of an expression.
ngSwitch
with enum.注意:我发现下面每个*ngSwitchCase
分支都只包含一个<p>
元素,所以我们可以将ng-container
拿掉,直接把*ngSwitchCase
放在<p>
标签上。
但是如果case分支中有多个元素,那么就需要用ng-container包裹起来。
default分支用*ngSwitchDefault
来标记。
1 | <!--app.component.html--> |
在Component中定义一个枚举类型,注意AccountStatus = AccountStatus;
这一行是必须的,否则编译报错。
1 | // app.component.ts |
@switch
.1 | <div class="use_built-in_control_flow"> |
Tips:
@default
branch at the end, if no case matched, and no @default
is provided, nothing will be rendered.fallthrough
in the @switch
directive, so you don’t need to add break
or return
in each @case
branch.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
.
ng-template
Structural directives
are used in ng-template
, here is an example:
1 | <ng-template [ngIf]="hero"> |
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 | <ng-container *ngIf="condition"> |
In this way, Angular will render the text directly without creating a div
element.
@if
With the new built-in structural @if directive, you can use the following syntax:
1 | @if (condition) { |
1 | <div class="case-3"> |
With @if
directive, you can use the following syntax, it’s more simple and readable:
1 | <div class="case-3"> |
既然Angular已经有ngIf
, ngFor
, ngSwitch等内置的结构性指令,为什么还要引入新的@if
,@for
, @switch
呢?主要有以下原因
ngIf
就会显得很臃肿。而使用@if
就可以很好的解决这个问题。@if
,@for
,@switch
编译成更高效的代码,这样可以减少运行时的开销。CommonModule
模块,直接使用即可,ngIf
等指令则需要导入CommonModule
模块。注意,RxJS中的scan
相当于JavaScript中的Array.prototype.reduce
,下面的例子展示了如何统计页面点击次数。
1 | const { fromEvent, scan } = rxjs; |
resize时间触发非常频繁,我们可以使用throttleTime
来节流,下面的例子展示了如何节流resize事件。
1 | const { fromEvent, throttleTime } = rxjs; |
throttleTime
可以用来实现节流,下面的例子展示了如何实现节流,无论用户点击的有多快,我们都是每隔1秒统计一次点击次数。
1 | const { fromEvent, throttleTime, scan } = rxjs; |
mergeMap
with concurrency
参数可以设置最大并发请求数,下面的例子展示了如何设置最大并发请求数。
1 | import { interval, mergeMap, of } from 'rxjs'; |
使用switchMap
可以避免重复请求,下面的例子展示了如何避免重复请求。
使用skip
, 以下代码忽略了interval
的前两个值,然后取3个值。所以忽略0和1,取2, 3, 4。关于interval的详细信息,请看这里
1 | import { interval, skip, take } from 'rxjs'; |
RxJS中有许多创建Observable的方法,如from
、of
、interval
等,本文一一介绍。
from
方法可以将一个数组、类数组对象、Promise、可迭代对象、Observable-like对象转换为Observable。
RxJS
社区有一个约定,以$
结尾的变量代表一个Observable对象。下面的例子将一个数组转换为Observable。
1 | import { from } from 'rxjs'; |
上面的代码可以简化为:
1 | import { from } from 'rxjs'; |
下面的例子将一个Promise对象转换为Observable。
1 | const p = new Promise((resolve) => { |
其实数组就是可迭代对象,所以前面数组的例子已经包含该例子,下面的例子将一个可迭代对象转换为Observable。
1 | function* generate123() { |
asapScheduler
使用asapScheduler
可以让from
方法在当前任务队列的末尾执行,下面的例子展示了如何使用asapScheduler
。你知道以下代码输出什么吗?如果不加asapScheduler
,输出会是什么?
1 | console.log('from start...'); |
以上代码输出如下:
1 | from start... |
如果去掉asapScheduler
,则from
同步执行,输出如下:
1 | from start... |
from
的以下书写方式已经被废弃,将在RxJS 8中移除.
1 | from([1, 2, 3], asapScheduler).subscribe(console.log); |
推荐使用下面的方式,详情看这里
1 | scheduled([1, 2, 3], asapScheduler).subscribe(console.log); |
使用fromEvent
方法可以将Dom事件转换为Observable。
以下代码监听document
的click
事件,当点击document
时,会在控制台输出clicked!
。
1 | <html> |
默认情况下,浏览器的事件模型是冒泡模型,也可以改为捕获模型,只需要传递{ capture: true }
作为fromEvent
的第三个参数即可。
1 | fromEvent(document, "click", { capture: true }).subscribe(() => { |
of
把输入的值转换为Observable。与from
不同的是,of
不会做任何flatten操作,它会把输入的值当做一个整体。这一点体现在数组上,from
会把数组的每个元素当做一个值,而of
会把整个数组当做一个值。
1 | import { of, from } from 'rxjs'; |
of
通常用在要一次性生成多个值的场景,比如下面的例子:
1 | import { of } from 'rxjs'; |
Emit a sequence of numbers in a range.
在一个范围内生成一个Observable。range
方法有三个参数,分别是start
、count
和scheduler
。start
表示起始值,count
表示生成的个数,scheduler
表示调度器。
1 | import { range } from 'rxjs'; |
interval
方法会每隔一段时间发出一个递增的数字。interval
方法有一个参数period
,表示时间间隔,单位是毫秒。interval
默认使用asyncScheduler
。
1 | import { interval } from 'rxjs'; |
有三点需要注意:
interval
方法是异步的,默认使用asyncScheduler
。interval
方法会从0开始发出数字。interval
方法第不是立即执行的,而是在第一个时间间隔之后执行。以下代码输出什么?
1 | console.log('interval start...'); |
答:因为interval
默认使用asyncScheduler
,所以interval
方法是异步的,所以interval start...
和interval end...
会先输出,然后再输出0, 1, 2, 3, 4, ...
。
可以将timer
看成是高配版的interval
,为啥这么说呢?因为interval
只能设置时间间隔,却无法控制开始时间(interval
的开始时间就是其时间间隔),而timer
既能控制间隔时间,也能控制开始时间。
来看这样一个需求,每个1秒产生一个数字(从0开始),但是要求第一个数字立即产生,而不是等待1秒。interval
无法满足这个需求,但是timer
可以。
1 | import { timer } from 'rxjs'; |
timer
方法的第一个参数就是开始时间,除了可以指定一个具体的毫秒数之外,还可以指定一个Date
对象,表示在指定的时间开始。
1 | timer(new Date(2024, 11, 28, 17, 0, 0), 1000).subscribe(console.log); // 0, 1, 2, 3, 4, ... |