0%

Introduction

arguments是一个类数组对象,包含了函数调用时传入的所有参数。arguments对象只有在函数内部才可以使用。

访问arguments对象

1
2
3
4
5
6
function add(a, b) {
console.log(arguments[0], arguments[1]);
return a + b;
}

add(1, 2); // output: 1 2

通过arguments对象可以改变函数参数的值

  1. 改变arguments时,函数参数的值也会改变。
1
2
3
4
5
6
function add(a, b) {
arguments[0] = 10;
console.log(a);
}

add(1, 2); // output: 10
  1. 改变函数参数时,arguments对象的值也会改变。
1
2
3
4
5
6
function add(a, b) {
a = 10;
console.log(arguments[0]);
}

add(1, 2); // output: 10

Introduction

try

catch

JavaScript中的catch不能按类型捕获异常,只能捕获所有异常。如果需要按类型捕获异常,可以使用if语句判断异常类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
myFunction();
} catch (e) {
if (e instanceof ReferenceError) {
console.error(`ReferenceError:${e.message}`);
} else if (e instanceof TypeError) {
console.error(`TypeError: ${e.message}`);
} else if (e instanceof SyntaxError) {
console.error(`SyntaxError: ${e.message}`);
} else if (e instanceof Error) {
console.error(`Error: ${e.message}`);
}
}

Java等其他类型的语言不同,JS不能像下面这样捕获异常。JS中一个try只能对应一个catch

1
2
3
4
5
6
7
8
9
10
11
try {
myFunction();
} catch (ReferenceError) {
console.error(`ReferenceError:${e.message}`);
} catch (TypeError) {
console.error(`TypeError: ${e.message}`);
} catch (SyntaxError) {
console.error(`SyntaxError: ${e.message}`);
} catch (Error) {
console.error(`Error: ${e.message}`);
}

finally

returnfinally同时存在时,finally会在return之前执行。

1
2
3
4
5
6
7
function test() {
try {
return 1;
} finally {
console.log('finally');
}
}

Introduction

Arrow function couldn’t be a constructor

Arrow function doesn’t have its own this binding

Arrow function has no prototype property

Arrow function has no arguments object

Arrow function has no hoisting.

Arrow function is not suitable for call, apply, bind methods

Introduction

JavaScript模板字符串是ES6新增的一种字符串形式,它可以让我们在字符串中插入变量,而不需要使用+号连接字符串,这样使得字符串的拼接更加简洁。以下是模板字符串的特点。

Interpolation

模板字符串中可以插入变量,使用${}包裹变量名。

1
2
3
const name = 'zdd';
const str = `Hello, ${name}`;
console.log(str); // Hello, zdd

Multiple lines of string

模板字符串可以跨行,这样我们就可以直接在字符串中换行,而不需要使用\n

1
2
3
const str = `Hello
World`;
console.log(str);

Tagged template

模板字符串还支持标签模板,即在模板字符串前面加上一个函数名,这个函数会处理模板字符串的内容。所以,这个模板字符串的应用场景是什么?在项目中好像没见过这么用的。

1
2
3
4
5
6
7
8
9
function tag(strings, ...values) {
console.log(strings); // ['Hello, ', '']
console.log(values); // ['zdd']
return 'Hello, ' + values[0];
}

const name = 'zdd';
const str = tag`Hello, ${name}`;
console.log(str); // Hello, zdd

Introduction

There are some questions about RxJS that are often asked in interviews. Let’s take a look at them.

What’s the difference between Promise and Observable?

  1. Promise是一对一的,而Observable可以是一对多的。(一个Observer可以有多个订阅者)
  2. Promise不能取消(配合某些Library也许可以),而Observable可以。
  3. Promise定义后立即执行,而Observable是惰性的。订阅后才执行。
  4. Promise可以配合async/await使用,而Observable不行。
  5. Promise没有操作符,而Observable有很多操作符,比如map,retry等,在处理复杂请求时更加方便。

Introduction

ngDoCheck runs before every time Angular checks a component’s template for changes.

常见的误解

很多人误以为,只要这个函数调用了,那么就证明Angular对当前组件进行了变更检测,这是一个常见的误解。注意看这个函数的定义:它是在每次Angular检查组件的模板变化之前运行的。所以不能以这个函数的调用作为Angular进行了变更检测的依据。

我们来看一个实际的例子:

  1. 一个父组件ParentComponent, 采用ChangeDetectionStrategyDefault模式
  2. 一个子组件ChildComponent,采用ChangeDetectionStrategy.OnPush模式
  3. 当父组件处理点击事件时,子组件的ngDoCheck函数会被调用吗?
  4. 再添加一个孙子组件GrandChildComponent,采用ChangeDetectionStrategy.OnPush模式,当父组件处理点击事件时,孙子组件的ngDoCheck函数会被调用吗?

ParentComponent采用默认的ChangeDetectionStrategyDefault模式,并且添加了一个按钮,当我们点击这个按钮时,Angular会触发变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// parent.component.ts
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
onParentButtonClick() {
console.log(`click in ParentComponent`);
}
}

ChildComponent采用ChangeDetectionStrategy.OnPush模式,只有当Input属性发生变化时,或者响应自身事件,或者手动触发了变更检测时,ngDoCheck函数才会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// child.component.ts
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<app-grand-child />
`,
styleUrl: './child.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements DoCheck {
ngDoCheck(): void {
console.log(`ngDoCheck in ChildComponent`);
}
}

GrandChildComponent同样采用了ChangeDetectionStrategy.OnPush模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// grand-child.component.ts
@Component({
selector: 'app-grand-child',
standalone: true,
imports: [],
template: `<p>grand-child works!</p>`,
styleUrl: './grand-child.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GrandChildComponent implements DoCheck {
ngDoCheck(): void {
console.log(`ngDoCheck in GrandChildComponent`);
}
}

当我们点击Parent按钮时,控制台输出包含如下一行,说明ChildComponent的ngDoCheck函数被调用了。

1
ngDoCheck in ChildComponent

可是我们明明指定了ChangeDetectionStrategy.OnPush模式,为什么会调用ngDoCheck函数呢?难道OnPush模式失效了吗?

其实不然,正如前面所说的,ngDoCheck函数是在每次Angular检查组件的模板变化之前运行的。所以,即使ChildComponent采用了ChangeDetectionStrategy.OnPush模式,ngDoCheck也被调用了,但是这并不意味着Angular对ChildComponent进行了变更检测。

这种情况只发生在OnPush根组件上,上面的GrandChildComponent并没有被调用,因为它是ChildComponent的子组件,所以它的ngDoCheck不会调用。

有此类行为的生命周期函数还有ngAfterViewChecked,无论Angular是否进行了变更检测,这个函数都会被调用。

如何确定Angular是否进行了变更检测?

对于一个组件来说,我如何确定Angular是否对它进行了变更检测呢?这个问题,其实困扰了我很久,以前我一直以为ngDoCheck函数的调用就是Angular进行了变更检测的标志,由上面的结论可知,这是不准确的。而其他生命周期函数也无法准确的告诉我们Angular是否进行了变更检测。

真的没办法了吗?

有的!其实之所以有这个困惑,还是对Angular变更检测理解不够深入,Angular的变更检测到底做了什么?其中必然有一个步骤是对template进行检查,如果template中绑定的值发生了变化,那么Angular就会更新视图。所以,我们可以通过template是否发生变化来判断Angular是否进行了变更检测。

代码很简单,只要在ChildComponent的模板中插入一个随机值即可,如果Angular进行了变更检测,那么每次这个值都会变化。如果这个值没有变,那么Angular就没有进行变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works! {{Math.random()}}</p>
<app-grand-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})

这时我们再点击Parent按钮,发现ChildComponent的模板中的随机值是不变的,这证明Angular没有进行变更检测。

ngDoCheck到底怎么用?

其实ngDoCheck是在Angular进行变更检测之前给用户一个机会,执行一些自定义逻辑。注意看官网的这句话:

1
You can use this lifecycle hook to manually check for state changes outside of Angular's normal change detection, manually updating the component's state.

所以到底要怎么使用它?恕我经验不够,我至今还未在项目中实际使用过ngDoCheck函数。但是我在网上找到了一些资料,可以参考一下:

  1. 使用第三方库时,如果你的组件中使用了第三方库,而第三方库改变了DOM状态,但是Angular没有检测到,这时可以使用ngDoCheck函数来手动检测状态变化。
  2. 自定义变更检测的逻辑,比如在某些条件下不进行变更检测,可以在ngDoCheck函数中实现。
  3. 子组件接收一个对象作为Input属性,而父组件只改变了对象的属性,这时ngOnChanges函数不会被调用的,这时可以使用ngDoCheck函数来检测对象的属性变化。
    说实话,这个例子非常的牵强,父组件只该变对象的一个属性这不是好的编程习惯,这种情况应该直接传递一个新的对象给子组件。但是作为例子,我们还是说一下这个情况。

使用第三方库

组件中使用第三方库改变了DOM状态,但是Angular没有检测到,这时可以使用ngDoCheck函数来手动检测状态变化。

1
2
3
ngDoCheck() {
this.cdf.markForCheck();
}

自定义变更检测逻辑

假设有一个父组件ParentComponent,一个子组件ChildComponent,父组件给子组件传递一个User对象,我们将User的年龄显示到子组件页面上,我们希望达到一个效果,如果年龄小于50岁时,子组件不进行变更检测,这种情况就需要使用ngDoCheck函数。

父组件定义如下,注意在onParentButtonClick函数中要重新赋值一个新的User对象,而不是改变User对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child [user]="user" />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
user: User = {
id: 1, name: 'Philip', age: 40,
}

onParentButtonClick() {
const age = Math.floor(Math.random() * 100) + 1;
this.user = {...this.user, age};
}
}

子组件定义如下,我们需要在ngDoCheck中自定义变更检测的逻辑。(注意,ngDoCheck在每次ngOnChanges之后调用。), 如果年龄小于50岁时,我们是用ChangeDetectorRef.detach()函数来停止变更检测,大于50岁时,我们使用ChangeDetectorRef.reattach()函数来恢复变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<p>{{user?.age}}</p>
<app-grand-child/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements DoCheck, OnChanges {
@Input() user: User | null = null;
currentAge: number | null = null;

constructor(private cdf: ChangeDetectorRef) {
}

ngOnChanges(changes: SimpleChanges): void {
this.currentAge = changes['user'].currentValue.age;
}

ngDoCheck(): void {
if (this.currentAge! < 50) {
this.cdf.detach(); // stop change detection
} else {
this.cdf.reattach(); // restore change detection
}
}
}

注意:这个例子十分牵强,现实中不会有这种情况的。

子组件接收对象作为Input属性,父组件只改变了对象的属性

User类型定义:

1
2
3
4
5
export interface User {
id: number;
name: string;
age: number;
}

父组件定义如下,初始化时,我们传递一个user对象给ChildComponent, 点击按钮时,改变user对象的age属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child [user]="user" />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
user: User = {
id: 1, name: 'Philip', age: 40,
}

onParentButtonClick() {
this.user.age = 18;
}
}

子组件定义如下,当父组件点击按钮时,子组件的页面上,age值并没有变化,还是40.
子组件的ngDoCheck函数会被调用,我们可以在ngDoCheck函数中检测user对象的属性变化,并打印出变化的值。
但是子组件的ngOnChanges函数不会被调用,因为父组件只改变了user对象的属性,而没有改变user对象本身。而ngOnChanges比较的是对象的引用,而不是对象内部的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<p>{{user?.age}}</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements OnInit, DoCheck, OnChanges {
@Input() user: User | null = null;
differ: any;

constructor(private differs: KeyValueDiffers) {
}

ngOnInit() {
this.differ = this.differs.find(this.user).create();
}

ngOnChanges(changes: SimpleChanges): void {
console.log(changes[0].currentValue.age);
}

ngDoCheck(): void {
console.log(`ngDoCheck in ChildComponent`);
const userChanges = this.differ.diff(this.user);
if (userChanges) {
userChanges.forEachChangedItem((changeRecord: any) => {
console.log('item changed : ' + changeRecord.key + ' ' + JSON.stringify(changeRecord.currentValue))
});
userChanges.forEachAddedItem((changeRecord: any) => {
console.log('item added : ' + changeRecord.key + ' ' + JSON.stringify(changeRecord.currentValue))
});
}
}
}

如果我们想让子组件页面上的age值变化,那么只需要手动触发一次变更检测即可。此时再点击父组件中的按钮,子组件的age值会变为18。

1
2
3
4
5
constructor(private cdf: ChangeDetectorRef) {}

ngDoCheck(): void {
this.cdf.markForCheck();
}

References

  1. https://angular.dev/guide/components/lifecycle#ngdocheck
  2. https://stackoverflow.com/a/45522199/1487475
  3. https://medium.com/@tehseen_ullah786/here-is-why-we-should-use-ngdocheck-in-angular-28bc98a86d85#:~:text=Integration%20with%20third%2Dparty%20libraries,and%20updates%20the%20view%20accordingly.

Introduction

ngOnChanges是Angular中非常重要的一个生命周期函数,今天我们来详细讨论一下它的用法。

Angular什么时候调用ngOnChanges

ngOnChanges是一个生命周期钩子,当Angular检测到组件的输入属性发生变化时,就会调用ngOnChanges函数。这意味着,只有当组件的输入属性发生变化时,ngOnChanges才会被调用。

我们什么时候需要重写ngOnChanges

通常情况下,我们不需要重写ngOnChanges函数,因为Angular会自动调用它。但是,以下几种情况都需要重写ngOnChanges函数:

  1. 当我们使用了OnPush时,且需要根据Input变化做相应操作时,那么就要响应ngOnChanges
  2. 当我们需要在组件的输入属性发生变化时执行一些自定义逻辑时,我们就需要重写ngOnChanges函数。

配合OnPush

假设有一个父组件:ParentComponent,一个子组件ChildComponent,父组件中调用一个API,返回值作为Input传递给子组件,子组件要根据这个Input继续调用另外一个API,这时候就需要在子组件中重写ngOnChanges函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component({
selector: 'app-child',
standalone: true,
imports: [],
template: `
<p>Child Component</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})

export class ChildComponent implements OnChanges {
@Input() data: any;

ngOnChanges(changes: SimpleChanges) {
if (changes.data) {
this.fetchData();
}
}

fetchData() {
// Call API
}
}

自定义逻辑

Introduction

Directives are classes that add additional behavior to elements in your Angular applications.

Angular has many kinds of directives, including components, attribute directives, and structural directives.

Component

This is the most common one we used every day, a component is a directive with a template. Components are the most basic building block of an Angular application.
https://angular.dev/guide/components

Structural Directives

Structural directives are responsible for HTML layout. They shape or reshape the DOM’s structure, typically by adding, removing, or manipulating elements.

NgIf

NgIf is used to conditionally creates or disposes of subviews in a template. see this post for more details.

NgFor

NgSwitch

Angular17引入了新的控制流语句:@if, @for, @switch,这些语句可以替代NgIf, NgFor, NgSwitch,更加简洁,高效,我们建议使用新的控制流语句。

Attribute Directives

Attribute directives are used to change the appearance or behavior of an element, component, or another directive. They are usually applied to elements as attributes.

Attribute directives doesn’t change the layout of the template, just change the appearance of it, for example, change the color, font size, visibility etc.

The most common attribute directives are as follows:

NgClass

NgStyle

NgModel

Customize Directives

如果内置的Directive不能满足我们的需求,我们可以自定义Directive,下面我们自定义一个Directive来实现一个简单Hover变色功能,当鼠标移动到元素上时,元素背景色变为红色,鼠标移出时恢复原背景色。

Create a directive

使用以下命令创建一个新的指令:HoverDirective,该命令会创建文件src/app/hover.directive.ts

1
ng g d hover

Implement the directive

以下是该指令的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {Directive, ElementRef, HostListener} from '@angular/core';

@Directive({
selector: '[appHover]',
standalone: true
})
export class HoverDirective {

constructor(private element: ElementRef) { }

@HostListener('mouseenter') onMouseEnter() {
this.setHoverColor('red')
}

@HostListener('mouseleave') onMouseLeave() {
this.setHoverColor('white');
}

private setHoverColor(color: string) {
this.element.nativeElement.style.backgroundColor = color;
}
}

简单解释一下上述代码:

  1. selector: appHover,这个是Directive的名字,我们在HTML中使用这个名字来引用这个Directive。
  2. constructor中注入了ElementRef,这个是Angular提供的一个服务,用来获取当前元素的引用。这里的当前元素就是使用了该Directive的元素。
  3. 两个HostListener分别监听mouseentermouseleave事件,当鼠标移入时,设置背景色为红色,移出时恢复原色。

Use the directive

首先,在需要使用这个指令的组件或者模块中的imports数组中导入这个Directive。

1
imports: [HoverDirective],

然后,在需要使用这个Directive的元素上加上appHover属性即可(注意:appHover就是定义Directive时指定的selector)。此时,当鼠标移入时,该段落的背景色会变为红色,移出时恢复原背景色。

1
<p appHover>Hover me</p>

这个自定义指令非常简单,虽然现在可以工作,但是有一个缺点,就是颜色的变化是固定的,如果我想让用户自定义Hover时的颜色,该怎么办呢?这就涉及到如何给自定义指令传递参数了。

我们修改一下实现,将颜色作为参数传递给Directive,这里我们定义了一个@Input属性,用来接收参数。@Input属性的名字是appHover,所以这里appHover同时作为Directive的selector和Input属性的名字。

1
2
3
4
5
6
7
// Add an input property to the directive
@Input('appHover') hoverColor: string = '';

// use this.hoverColor in setHoverColor method
@HostListener('mouseenter') onMouseEnter() {
this.setHoverColor(this.hoverColor || 'red')
}

另外,为了防止用户忘记传递参数,我们给hoverColor设置了一个默认值'red',这样即使用户不传递参数,也不会报错。

现在,我们可以在HTML中传递颜色参数了。

1
<p appHover="green">Hover me</p>

上面的例子只有一个参数,所以我们在定义@Input的时候,直接把参数定义到了Directive的selector上,如果有多个参数,这种方法就不好使了,为了说明问题,我们再加一个参数,字体颜色,就是当鼠标Hover时改变背景色,同时也改变字体颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Directive, ElementRef, HostListener, Input} from '@angular/core';

@Directive({
selector: '[appHover]',
standalone: true
})
export class HoverDirective {
@Input('hoverBackgroundColor') hoverColor: string = '';
@Input('hoverFontColor') hoverFontColor: string = '';

constructor(private element: ElementRef) { }

@HostListener('mouseenter') onMouseEnter() {
this.setHoverColor(this.hoverFontColor || 'black', this.hoverColor || 'red')
}

@HostListener('mouseleave') onMouseLeave() {
this.setHoverColor('black', 'white');
}

private setHoverColor(fontColor: string, backgroundColor: string) {
this.element.nativeElement.style.color = fontColor;
this.element.nativeElement.style.backgroundColor = backgroundColor;
}
}

使用时:

1
<p appHover hoverFontColor="red" hoverBackgroundColor="yellow">This is a paragraph</p>

Angular Interview Questions

总结一些Angular的面试题,希望对大家有所帮助。

  1. 简单描述Angular框架的特点,可以对比其他框架,比如React、Vue等。
  2. Angular的生命周期钩子有哪些,分别在什么时候调用?详情
  3. Angular数据绑定方式有哪些?详情
  4. Angular指令是什么?有哪些种类?详情
  5. Angular View Encapsulation有哪些方式?详情
  6. :host::ng-deep的作用是什么?详情
  7. Angular的依赖注入是如何实现的?详情
    1. Angular依赖注入有哪些Injector?
    2. 提供Provider的方式有哪些?
    3. 如果Service带参数,该如何为其提供Provider? - 使用useFactory。- 唯有此法能传参数。
  8. Angular的路由是如何实现的?详情
  9. Angular的变更检测是如何实现的?Change Detection
    1. 基于zone.js的变更检测
    2. zoneless变更检测
    3. OnPush变更检测的原理是怎样的?如果有子组件呢?详情
    4. detectChangesmarkForCheck有什么区别? [详情](https://zdd.github.io/2024/12/16/angular-change-detection-markforcheck/)
    5. runOutsideAngular的作用是什么?详情
  10. Angular组件间通信有哪些方式?各有什么优缺点?详情
  11. Angular如何实现性能优化?
  12. Angular injection context有哪些?[详情](https://zdd.github.io/2025/02/18/angular-di-injection-context/)
  13. Angular @Input, @Input
  14. Angular Form - 重点复习一下,比如如何自定义验证器,如何自定义表单控件等。

Angular新特性

  1. 什么是signal? 详情
  2. signal与OnPush的关系,对于使用了OnPush更新策略的组件,如果内部使用了signal,Angular会将signal视为该组件的一个依赖,当这个signal变化时,Angular会将组件标记为待更新(markForCheck)。
  3. Angular deferrable view, 详情
  4. Angular SSR - 这个要重点复习一下。

What is Angular?

Angular is a web framework which help developers to build fast and scalable web applications. It is developed and maintained by Google.

It’s a MVC(or MVVM) framework to build SPA (Single Page Application) using HTML, CSS and JavaScript/TypeScript.

Angular has a lot of features like:

  1. Two-way data binding
  2. Dependency Injection
  3. Routing
  4. Directives
  5. Pipes
  6. Services
  7. Forms
  8. HttpClient
  9. Animations
  10. SSR (Server Side Rendering)

Differences between Angular and AngularJS

AngularJS Angular
Version 1.x 2+
Language JavaScript TypeScript
Architecture Controller Components
Mobile Support No Yes(Ionic)
CLI No Yes
Lazy loading No Yes
SSR No Yes

What are directives in Angular?

Directives are classes that add additional behavior to elements in your Angular applications. Angular has many kinds of directives, including:

  • Component
  • Attribute Directives
    • NgClass
    • NgStyle
    • NgModel
  • Structural Directives
    • NgIf
    • NgFor
    • NgSwitch

Component vs Module

Component is where you write you binding code, and Module groups components. An app contains many modules, and a module contains many components.

1
2
3
4
5
6
app
├─ Login Module
│ ├─ Login Component
│ ├─ Register Component
│ ├─ Forgot Password Component
│ ├─ Reset Password Component

Before standalone component, Two components in different module can’t communicate with each other directly, they need to communicate by their enclosing module. See here for more details.

Angular and MVVM

Angular is a MVVM framework, it’s a bit different from MVC.

  • Model: Data types and Services
  • View: HTML template
  • ViewModel: Component

In MVVM pattern, Model and View doesn’t communicate directly. ViewModel is a mediator between Model and View, it’s a class that contains the business logic of the application. In Angular, ViewModel is a Component.

What’s signal in Angular?

What’s deferrable component?

Angular如何实现性能优化?

  1. 使用OnPush策略 - reduce change detection counts.
  2. 使用trackBy
  3. 使用lazy load components - 減少initial bundle size, decrease first screen loading time.
  4. 图片优化:https://angular.dev/tutorials/learn-angular/11-optimizing-images - increase images loading speed.
  5. 合并API call(特别是使用GraphQL时),減少API call次数。这项优化非Angular特有,但是在Angular中也是适用的。
  6. 减小bundle size - 使用tree shaking, code splitting, lazy loading等技术。也要分析哪些npm包体积过大,是否有替代品。

#工作中遇到的真实案例

ng-if问题

之前一个同事写了一个很大的组件,就是一个左侧边栏菜单,这个边栏要加载用户账号,有的用户有上千个账号,而这个边栏还有一个收起和展开的操作。测试人员发现,当用户的账号很多时,收起和展开就特别卡。调查发现,收起和展开是通过ng-if实现的,而ng-if在显示和隐藏时是要操作DOM将对应的组件重建或删除的。这个组件很大,所以导致了卡顿。解决方案是使用ng-show/ng-hide,这样就不会导致组件的重建和删除。

References:

  1. https://dev.to/renukapatil/angular-interview-questions-4i4k 3.

In Angular, the resolution modifier is used to control how the dependency injector resolves dependencies. There are four resolution modifiers in Angular:

先来给这几个操作符分个类:

  1. @SkipSelf - 何时开始查找。
  2. @Self, @Host - 何时停止查找。
  3. @Optional - 找不到时怎么办。

@Optional

这个是最简单了修饰符了,它告诉Angular依赖注入器,如果找不到依赖项,不要抛出错误,而是返回null

大家都见过下面这个错误吧,当Angular找不到某个依赖时,就会抛出这个错误:(注意:这种错误一般发生在某个Service没有提供{providedIn: 'root'},而且该Service的使用者也没有在providers数组中提供该Service时。)

1
2
3
4
5
6
7
8
9
10
11
12
ERROR NullInjectorError: R3InjectorError(Environment Injector)[_OrderService -> _OrderService]: 
NullInjectorError: No provider for _OrderService!
at NullInjector.get (core.mjs:1635:21)
at R3Injector.get (core.mjs:3017:27)
at R3Injector.get (core.mjs:3017:27)
at ChainedInjector.get (core.mjs:5288:32)
at lookupTokenUsingModuleInjector (core.mjs:5631:31)
at getOrCreateInjectable (core.mjs:5677:10)
at Module.ɵɵdirectiveInject (core.mjs:11586:17)
at NodeInjectorFactory.ProductComponent_Factory [as factory] (product.component.ts:21:30)
at getNodeInjectable (core.mjs:5871:38)
at createRootComponent (core.mjs:16405:31)

如果我们在依赖注入的时候加上@Optional()修饰符,那么Angular就不会抛出错误,而是返回null

1
2
3
constructor(@Optional() private orderService: OrderService) {
this.orderService.fetchData(1).then(console.log);
}

注意:上面这种写法,在使用orderService时,需要判断是否为null,否则仍然会抛出Cannot read properties of null (reading 'fetchData')的错误。

我们可以针对orderService使用?(Optional Chaining Operator),就不会抛出错误了:

1
2
3
constructor(@Optional() private orderService: OrderService) {
this.orderService?.fetchData(1).then(console.log);
}

@Self

@Self也比较简单,就是告诉Angular只在当前Component或者Directive中搜索某个依赖项,而不会去父级Component或者Directive中搜索。假设有OrderService,且是providedIn: 'root'的。

1
2
@Injectable({providedIn: "root"})
export class OrderService {}

如果我们在注入该Service时,加上@Self()修饰符,那么Angular只会在当前Component或者Directive中搜索该Service,但是当前Component或者Directive中没有提供该Service时,所以就会抛出错误。

1
2
3
constructor(@Self() private orderService: OrderService) {
this.orderService.fetchData(1).then(console.log);
}

注意观察这个错误和上面错误的区别,这个错误的顶端不再是NullInjector了,因为我们只在当前Component中查找,所以永远不会到达NullInjector这一步。而NodeInjector是当前Component的Injector。

1
2
3
4
5
6
7
8
9
10
11
core.mjs:7191 ERROR RuntimeError: NG0201: No provider for _OrderService found in NodeInjector. Find more at https://angular.dev/errors/NG0201
at throwProviderNotFoundError (core.mjs:986:9)
at notFoundValueOrThrow (core.mjs:5606:5)
at lookupTokenUsingModuleInjector (core.mjs:5639:10)
at getOrCreateInjectable (core.mjs:5677:10)
at Module.ɵɵdirectiveInject (core.mjs:11586:17)
at NodeInjectorFactory.ProductComponent_Factory [as factory] (product.component.ts:21:30)
at getNodeInjectable (core.mjs:5871:38)
at createRootComponent (core.mjs:16405:31)
at ComponentFactory.create (core.mjs:16262:21)
at ViewContainerRef2.createComponent (core.mjs:16662:43)

@Self@Optional是可以配合使用的,为了修复上面的错误,我们可以这样写,这样就不会报错了。

1
2
3
constructor(@Self() @Optional() private orderService: OrderService) {
this.orderService?.fetchData(1).then(console.log);
}

所以,@Self修饰符的使用场景到底是啥?请继续往下看。

Multiple Directives in same Dom elements

如果一个DOM元素上应用了多个Directive, 那么当第一个Directive提供了某个Service时,后面的Directive就可以通过@Self来获取这个Service。

下面的代码,假设dir1对应Directive1,dir2对应Directive2,如果Directive1中的providers数组提供了某个Service,那么Directive2中constructor可以使用@Self来获取Service。

1
<p dir1 dir2>Hello, world!</p>

Directive1中提供了OrderService

1
2
3
4
5
6
7
8
// Directive1
@Directive({
selector: '[dir1]',
providers: [OrderService],
})
export class Dir1Directive {
constructor() { }
}

Directive2中使用@Self来获取OrderService。

1
2
3
4
5
6
7
// Directive2
@Directive({
selector: '[dir2]'
})
export class Dir2Directive {
constructor(@Self() private orderService: OrderService) { }
}

为什么这样可以呢?因为dir1dir2位于同一个dom元素-p标签上,Angular会为这个p标签创建一个Injector,dir1dir2会share这个Injector,所以dir2可以通过@Self来获取dir1中提供的OrderService。

@Host

@Host修饰符告诉Angular依赖注入器,只在当前Component或者Host元素中搜索某个依赖项。

  • 当不使用ng-content时,Host就是注入依赖的Component。
  • 当使用ng-content时,Host就是投射内容的Component(也就是ng-content所在的Component)。see here for details.

@Host的作用体现在使用ng-content进行内容投射的时候。下面的例子中,我们有一个ProductComponent,它的模板中有ng-content,我们将ProductDetailComponent投射到ProductComponent中。- 此时,ProductComponent就是ProductDetailComponent的Host。

1
2
3
4
5
6
<!-- product.component.html -->
<div class="container">
<div class="header"></div>
<ng-content></ng-content>
<div class="footer"></div>
</div>

然后我们在ProductComponent中提供OrderService,并在ProductDetailComponent中使用@Host来获取OrderService。

1
2
// product.component.ts
providers: [OrderService],
1
2
// product-detail.component.ts
constructor(@Host() private orderService: OrderService) {}

最后在AppComponent的模板中使用ProductComponent

1
2
3
4
<!--app.component.html-->
<app-product>
<app-product-detail></app-product-detail>
</app-product>

这时,app-product就是app-product-detailHost,如果我们在ProductDetailComponent中使用@Host来获取OrderService,那么Angular会首先在ProductDetailComponent中查找,结果没找到,然后再去ProductComponent中查找OrderService,结果找到了。

@SkipSelf

@SkipSelf就是查找依赖时不在当前Component或者Directive中查找,而是跳过当前Component或者Directive,去父级Component或者Directive中查找。

所以,@SkipSelf修饰符的使用场景到底是啥?

References:

  1. https://v17.angular.io/guide/dependency-injection-in-action#make-a-dependency-optional-and-limit-search-with-host - 关于Host的定义,可以看这里。