1. 如何理解Angular Signal
Angular Signals is a system that granularly tracks how and where your state is used throughout an application, allowing the framework to optimize rendering updates.
什么是信号
信号是一个值的包装器,可以在该值发生变化时通知感兴趣的消费者。信号可以包含任何值,从简单的原语到复杂的数据结构。
信号的值总会通过 getter 函数读取,这使得 Angular 可以跟踪信号的使用位置。
信号可以是可写的或只读的。
可写信号
可写信号提供了一个 API 来直接更新它们的值。你可以通过使用信号的初始值调用 signal 函数来创建可写信号:
const count = signal(0);
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());
要更改可写信号的值,你可以直接 .set():
count.set(3);
或者使用 .update() 操作从前一个值计算出一个新值:
// Increment the count by 1.
count.update(value => value + 1);
在处理包含对象的信号时,有时直接改变该对象很有用。例如,如果对象是一个数组,你可能希望在不完全替换数组的情况下推送一个新值。要进行这样的内部更改,请使用 .mutate 方法:
const todos = signal([{title: 'Learn signals', done: false}]);
todos.mutate(value => {
// Change the first TODO in the array to 'done: true' without replacing it.
value[0].done = true;
});
计算信号
计算信号是从其他信号中派生出来的。可以使用 computed 并指定推导函数来定义一个:
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
doubleCount 信号取决于 count。每当 count 更新时,Angular 知道任何依赖于 count 或 doubleCount 东西也需要更新。
计算信号都是惰性计算和记忆的
在第一次读取 doubleCount 之前,不会运行 doubleCount 的派生函数以计算其值。一旦计算出来,这个值就会被缓存起来,以后读取 doubleCount 将返回缓存的值而不用重新计算。
当 count 发生变化时,它会告诉 doubleCount 它的缓存值不再有效,并且该值只会在下一次读取 doubleCount 时重新计算。
因此,在计算信号中执行计算量大的推导(例如过滤数组)是相当安全的。
计算信号不是可写信号
你不能直接为计算信号赋值。比如,
doubleCount.set(3);
会产生编译错误,因为 doubleCount 不是 WritableSignal。
计算信号的依赖性是动态的
只能跟踪推导期间实际读取过的信号。例如,在此计算中,只会有条件地读取 count 信号:
const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`;
} else {
return 'Nothing to see here!';
}
});
读取 conditionalCount 时,如果 showCount 为 false,则没有读取 count 信号就返回了消息 “Nothing to see here!”。这意味着对 count 的更新不会导致重新计算。
如果稍后将 showCount 设置为 true 并再次读取 conditionalCount,则将重新执行派生并采用 showCount 为 true 的分支,返回显示 count 值的消息。对 count 的更改将使 conditionalCount 的缓存值无效。
请注意,可以删除和添加依赖项。如果 showCount 稍后再次设置为 false,则 count 将不再被视为 conditionalCount 的依赖项。
何时使用副作用
在大多数应用程序代码中很少需要副作用,但在特定情况下可能会有用。下面是一些需要以 effect 作为解决方案的例子:
-
记录正在显示的数据及其更改时间,用于分析或作为调试工具
-
在数据与 window.localStorage 之间保持同步
-
添加无法用模板语法表达的自定义 DOM 行为
-
对 canvas、图表库或其他第三方 UI 库执行自定义渲染
如何理解 Web Worker
在Angular中,Web Worker是一种运行在浏览器后台线程的JavaScript,它允许你执行任务而不会冻结用户界面。这对于执行密集型计算或长时间运行的任务特别有用,因为这些任务可能会阻塞主线程,导致用户界面变得不响应。
主线程与工作线程:
主线程:负责处理UI交互和DOM操作。
工作线程:运行Web Worker,处理后台任务,不会直接影响UI。
避免UI阻塞:
通过将计算密集型任务放在Web Worker中执行,可以避免主线程阻塞,从而提高应用的响应性和性能。
数据交换:
主线程和工作线程之间通过消息传递进行通信。你可以使用postMessage方法发送消息给Worker,Worker也可以通过postMessage向主线程发送消息。
创建Web Worker:
在Angular中,你可以创建一个服务来管理Web Worker。这个服务负责创建Worker实例,发送消息,并处理从Worker接收到的消息。
Angular服务中的Web Worker:
你可以使用@angular/platform-webworker和@angular/platform-webworker动态模块来创建专门的Worker应用程序或共享Worker。
示例:
以下是一个简单的Angular服务,演示如何使用Web Worker:
// worker.service.ts
import { Injectable } from '@angular/core';
import { Worker } from '@angular/platform-webworker';
@Injectable()
export class WorkerService {
private worker: Worker;
constructor() {
this.worker = new Worker('./worker', { data: 'Hello, worker!' });
this.worker.onMessage().subscribe(message => {
console.log('Message from worker:', message);
});
}
sendToWorker(data: any) {
this.worker.postMessage(data);
}
}
2 Web Worker 使用场景:
CPU密集型计算:
Web Worker允许在后台线程中运行CPU密集型计算,从而释放主线程以更新用户界面。这对于需要进行大量计算的应用,如生成CAD图纸或进行繁重的几何计算,非常有帮助
离线数据处理和缓存:
通过使用Web Worker和缓存,可以提高应用的性能和响应性。用户可以继续在页面上进行其他操作,而不会受到耗时计算的影响。同时,由于计算结果已经存储在缓存中,当用户需要再次获取时,可以快速地获取到,提高了应用的效率
大量数据计算:
对于需要处理大量数据的Web应用,例如数据分析工具或科学计算应用程序,可以使用Web Worker来处理这些数据。这样可以避免主线程阻塞,提高应用性能
图像处理和媒体编解码:
Web Worker在图像处理和媒体编解码方面也有广泛的应用。例如,可以创建一个Web Worker线程来处理图像调整大小、旋转、裁剪、滤镜等操作,而不阻塞主线程
实时数据推送和通知:
对于需要实时推送数据更新和通知的应用,可以使用Web Worker来处理实时数据的获取和处理任务。这样可以在后台线程中使用WebSocket、Server-Sent Events或定期轮询来获取实时数据,而不会影响主线程的用户交互
轮询服务器状态:
有时,浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工作可以放在Web Worker里面,以避免阻塞主线程
文件数据处理:
在某些应用中,可能需要处理大量文件数据,如保存文件数据到服务器。这些操作可以在Web Worker中进行,以避免阻塞用户界面
3. Angular 生命周期Hooks
Angular的生命周期钩子是组件或指令在其生命周期中经历的不同阶段的回调方法。理解这些钩子有助于我们更好地控制组件的行为。以下是Angular组件的生命周期钩子及其用途:
1. ngOnChanges
- 触发时机: 当输入属性(通过
@Input
装饰器标记的属性)发生变化时。 - 用途: 可以用来检测输入属性的变化,并执行相应的逻辑。
- 操作示例: 重新计算数据、重新渲染视图、更新状态等。
2. ngOnInit
- 触发时机: 组件或指令被初始化后,且第一次
ngOnChanges
之后。 - 用途: 用于执行初始化逻辑,如订阅Observables、获取数据、设置初始状态等。
- 操作示例: 发起HTTP请求、初始化表单、设置默认值等。
3. ngDoCheck
- 触发时机: 每次变更检测运行时。
- 用途: 用于检测Angular自身未检测到的变化,如第三方库的变化。
- 操作示例: 手动检查变量的变化、执行自定义变更检测逻辑等。
4. ngAfterContentInit
- 触发时机: 组件内容投影完成后。
- 用途: 用于在内容投影完成后执行逻辑。
- 操作示例: 初始化投影内容、设置初始状态等。
5. ngAfterContentChecked
- 触发时机: 每次变更检测运行后,内容投影检查完成后。
- 用途: 用于在内容投影检查完成后执行逻辑。
- 操作示例: 更新投影内容的状态、执行额外的检查等。
6. ngAfterViewInit
- 触发时机: 组件视图初始化完成后。
- 用途: 用于在视图初始化完成后执行逻辑。
- 操作示例: 初始化视图、设置初始状态、操作DOM等。
7. ngAfterViewChecked
- 触发时机: 每次变更检测运行后,视图检查完成后。
- 用途: 用于在视图检查完成后执行逻辑。
- 操作示例: 更新视图的状态、执行额外的检查等。
8. ngOnDestroy
- 触发时机: 组件或指令被销毁前。
- 用途: 用于执行清理工作,如取消订阅、清理定时器等。
- 操作示例: 取消HTTP请求、清理DOM监听器、释放资源等。
示例代码
import { Component, OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy, Input } from '@angular/core';
@Component({
selector: 'app-lifecycle',
template: `<p>Current value: {{ value }}</p >`
})
export class LifecycleComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input() value: string;
constructor() {
console.log('Constructor called');
}
ngOnChanges(changes: SimpleChanges): void {
console.log('ngOnChanges called', changes);
}
ngOnInit(): void {
console.log('ngOnInit called');
}
ngDoCheck(): void {
console.log('ngDoCheck called');
}
ngAfterContentInit(): void {
console.log('ngAfterContentInit called');
}
ngAfterContentChecked(): void {
console.log('ngAfterContentChecked called');
}
ngAfterViewInit(): void {
console.log('ngAfterViewInit called');
}
ngAfterViewChecked(): void {
console.log('ngAfterViewChecked called');
}
ngOnDestroy(): void {
console.log('ngOnDestroy called');
}
}
总结
- ngOnChanges: 输入属性变化时调用。
- ngOnInit: 初始化完成后调用。
- ngDoCheck: 每次变更检测时调用。
- ngAfterContentInit: 内容投影完成后调用。
- ngAfterContentChecked: 内容投影检查完成后调用。
- ngAfterViewInit: 视图初始化完成后调用。
- ngAfterViewChecked: 视图检查完成后调用。
- ngOnDestroy: 销毁前调用。
4. 如何理解Angular中的依赖注入
依赖注入(DI)是 Angular 中的基本概念之一。DI 被装配进 Angular 框架,并允许带有 Angular 装饰器的类(例如组件、指令、管道和可注入对象)配置它们所需的依赖项。
DI 系统中存在两个主要角色:依赖使用者和依赖提供者。
Angular 使用一种称为 Injector 的抽象来促进依赖消费者和依赖提供者之间的互动。当有人请求依赖项时,注入器会检查其注册表以查看那里是否已有可用的实例。如果没有,就会创建一个新实例并将其存储在注册表中。Angular 会在应用的引导过程中创建一个应用范围的注入器(也称为“根”注入器),并会根据需要创建任何其它注入器。在大多数情况下,你都不需要手动创建注入器,但应该知道有这样一个连接提供者和消费者的层次。
本主题介绍了某个类如何作为依赖项的基本场景。Angular 还允许你使用函数、对象、基本类型(例如字符串或 Boolean)或任何其他类型作为依赖项。
提供依赖项
假设有一个名为 HeroService 的类需要用作组件中的依赖项。
第一步是添加 @Injectable 装饰器以表明此类可以被注入。
@Injectable()
class HeroService {}
下一步是提供它,以便让其在 DI 中可用。可以在多种地方提供依赖项:
在组件级别,使用 @Component 装饰器的 providers 字段。在这种情况下,HeroService 将可用于此组件的所有实例以及它的模板中使用的其他组件和指令。例如:
@Component({
selector: 'hero-list',
template: '...',
providers: [HeroService]
})
class HeroListComponent {}
当你在组件级别注册提供者时,该组件的每个新实例都会获得一个新的服务实例。
在 NgModule 级别,要使用 @NgModule 装饰器的 providers 字段。在这种情况下,HeroService 可用于此 NgModule 或与本模块位于同一个 ModuleInjector 的其它模块中声明的所有组件、指令和管道。当你向特定的 NgModule 注册提供者时,同一个服务实例可用于该 NgModule 中的所有组件、指令和管道。要理解所有边缘情况,参见多级注入器。例如:
@NgModule({
declarations: [HeroListComponent]
providers: [HeroService]
})
class HeroListModule {}
在应用程序根级别,允许将其注入应用程序中的其他类。这可以通过将 providedIn: ‘root’ 字段添加到 @Injectable 装饰器来实现:
@Injectable({
providedIn: 'root'
})
class HeroService {}
当你在根级别提供服务时,Angular 会创建一个 HeroService 的共享实例,并将其注入到任何需要它的类中。在 @Injectable 元数据中注册提供者还允许 Angular 通过从已编译的应用程序中删除没用到的服务来优化应用程序,这个过程称为摇树优化(tree-shaking)。
注入依赖项
注入依赖项的最常见方法是在类的构造函数中声明它。当 Angular 创建组件、指令或管道类的新实例时,它会通过查看构造函数的参数类型来确定该类需要哪些服务或其他依赖项。例如,如果 HeroListComponent 要用 HeroService,则构造函数可以如下所示:
@Component({ … })
class HeroListComponent {
constructor(private service: HeroService) {}
}
当 Angular 发现一个组件依赖于一项服务时,它会首先检查注入器中是否已有该服务的任何现有实例。如果所请求的服务实例尚不存在,注入器就会使用注册的提供者创建一个,并在将服务返回给 Angular 之前将其添加到注入器中。
当所有请求的服务都已解析并返回时,Angular 就可以用这些服务实例为参数,调用该组件的构造函数。
指定提供者令牌
如果你用服务类作为提供者令牌,则其默认行为是注入器使用 new 运算符实例化该类。
在下面这个例子中,Logger 类提供了 Logger 的实例。
providers: [Logger]
但是,你可以将 DI 配置为使用不同的类或任何其他不同的值来与 Logger 类关联。因此,当注入 Logger 时,会改为使用这个新值。
实际上,类提供者语法是一个简写表达式,可以扩展为由 Provider 接口定义的提供者配置信息。
在这种情况下,Angular 将 providers 值展开为完整的提供者对象,如下所示:
[{ provide: Logger, useClass: Logger }]
展开后的提供者配置是一个具有两个属性的对象字面量:
provide 属性包含一个令牌,该令牌会作为定位依赖值和配置注入器时的键。
第二个属性是一个提供者定义对象,它会告诉注入器如何创建依赖值。提供者定义对象中的键可以是以下值之一:
-
useClass - 此选项告诉 Angular DI 在注入依赖项时要实例化这里提供的类
-
useExisting - 允许你为令牌起一个别名,并引用任意一个现有令牌。
-
useFactory - 允许你定义一个用来构造依赖项的函数。
-
useValue - 提供了一个应该作为依赖项使用的静态值。
5 Angular常见指令有哪些
NgIf:
用于根据条件包含或排除一个元素。
<div *ngIf="condition">Content to show when condition is true.</div>
NgForOf:
用于基于一个数组或可迭代对象来重复元素。
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
NgSwitch:
用于在多个条件之间切换显示不同的内容。
<div [ngSwitch]="condition">
<ng-container *ngSwitchCase="'case1'">Case 1</ng-container>
<ng-container *ngSwitchDefault>Default case</ng-container>
</div>
NgClass 和 NgStyle:
用于动态添加或移除CSS类和样式。
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}"></div>
<div [ngStyle]="{'color': color, 'font-size': size}"></div>
NgModel:
用于实现双向数据绑定,通常与表单控件一起使用。
<input type="text" [(ngModel)]="name">
FormsModule:
提供了NgModel以及其他表单相关的指令,需要在模块中导入。
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [FormsModule]
})
NgTemplateOutlet:
用于插入一个模板到指定的位置。
<ng-template let-item="item">
<div>{{ item }}</div>
</ng-template>
<ng-container *ngTemplateOutlet="template; context: { $implicit: context }"></ng-container>
NgComponentOutlet:
用于动态创建组件实例。
<ng-component-outlet [component]="dynamicComponent"></ng-component-outlet>
NgIfElse:
与NgIf一起使用,用于提供条件为假时的备用内容。
<div *ngIf="condition; else elseBlock">True</div>
<ng-template #elseBlock>False</ng-template>
AsyncPipe:
用于订阅可观察对象,并自动清理订阅。
<div>{{ (data$ | async)?.property }}</div>
NgNonBindable:
用于跳过元素及其子元素的Angular绑定。
<span ngNonBindable>{{ '{' }}{{ value }}{{ '}' }}</span>
NgContent:
用于在组件中分配内容(使用标签)。
NgHost:
用于在创建组件时指定宿主元素。