学习Angular的AsyncPipe
这篇文章是2022年Angular圣诞日历上第20天的文章。
这是关于学习AsyncPipe的记录。
异步管道是什么?
首先,我們先確認官方網頁。
异步管道订阅 Observable 或 Promise,并返回其已发出的最新值。当有新值被发出时,异步管道标记该组件以进行变更检查。当组件被销毁时,异步管道会自动取消订阅,以避免潜在的内存泄漏。当表达式的引用发生变化时,异步管道会自动取消订阅旧的 Observable 或 Promise,并订阅新的。
-
- 通过订阅Observable(或Promise),返回最新的值。
当组件被销毁时,AsyncPipe会自动取消订阅。
当引用的表达式发生变化时,AsyncPipe会自动取消订阅旧的Observable并订阅新的Observable。
使用AsyncPipe的好处是什么?
-
- コンポーネントでの購読解除に関するコードを書かなくて良い
購読解除忘れを心配をしなくて良い(購読解除忘れはメモリリークを引き起こす)
コンポーネントのコード量が減りスッキリする
Observableの初期化をコンストラクタで完結させることができる
readonlyにできる
就第2.3个原因而言,作为一个初学者,我并没有太多体会到其中的乐趣,所以如果找到一个好的例子,我会再次写下来。
就第1个原因而言,通过比较引入 AsyncPipe 前后的代码,一目了然。
import { Component, OnDestroy, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'user-list',
template: `
<ul>
<li *ngFor="let user of users">
{{ user.id }} {{ user.first_name }} {{ user.last_name }}
</li>
</ul>
`,
})
export class UserListComponent implements OnDestroy, OnInit {
private _onDestroy = new Subject<void>();
users: User[];
constructor(private userListService: UserListService) { }
ngOnInit() {
this.userListService.getUsers()
.pipe(takeUntil(this.onDestroy))
.subscribe(users => {
this.users = users;
});
}
ngOnDestroy() {
this._onDestroy.next(null);
}
}
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';
@Component({
selector: 'user-list',
template: `
<ul>
<li *ngFor="let user of users$ | async">
{{ user.id }} {{ user.first_name }} {{ user.last_name }}
</li>
</ul>
`,
})
export class UserListComponent implements OnInit { // だいぶスッキリした
readonly users$: Observavle<User[]>;
constructor(private userListService: UserListService) {
this.users$ = this.userListService.getUsers();
}
ngOnInit() {
}
}
在组件内,有关取消订阅的描述完全消失了,感觉非常清爽。
然而,异步管道也有令人困扰的地方。
初始值为空问题 zhí
在使用AsyncPipe时,尽管流(Stream)中绝对不会出现null,但其子组件却不得不关注null。
假设在以下情况下,会在编译时引发错误。
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';
@Component({
selector: 'app-root',
template: `<user-list [users]="users$ | async"></user-list>`,
})
export class AppComponent implements OnInit {
readonly users$: Observavle<User[]>;
constructor(private userListService: UserListService) {
this.users$ = this.userListService.getUsers();
}
ngOnInit() {
}
}
@Component({
selector: 'user-list',
template: `
<ul>
<li *ngFor="let user of users">
{{ user.id }} {{ user.first_name }} {{ user.last_name }}
</li>
</ul>
`,
})
export class UserListComponent implements OnInit {
@Input() users: User[];
constructor() { }
ngOnInit() {
}
}
Type 'null' is not assignable to type 'User[]'.
1 <user-list [users]="users$ | async"></user-list>
~~~~~
如果将null放入User[]类型的users变量中则不行!!
如下所示,UserListService的getUsers()方法不会返回null。
@Injectable({ providedIn: 'root' })
export class UserListService {
constructor(private http: HttpClient) { }
getUsers(): Observable<User[]> {
return this.http
.get<{ data: User[] }>("https://reqres.in/api/users")
.pipe(
map(resp => resp.data)
);
}
}
尽管用户为空但仍然进入了null的原因在于AsyncPipe的实现。
这篇文章详细解释了,但因为这篇文章是在2020年写的,代码有些变动,所以我会再次确认。
(我复制了AsyncPipe并创建了一个名为MyAsyncPipe的管道,然后快乐地加入了console.log()。?)
transform()函数是关键代码。
@Pipe({
name: 'async',
pure: false,
standalone: true,
})
export class AsyncPipe implements OnDestroy, PipeTransform {
private _latestValue: any = null; //<- コイツが問題
~~~~
private _obj: Subscribable<any>|Promise<any>|EventEmitter<any>|null = null;
// 略
transform<T>(obj: Observable<T>|Subscribable<T>|Promise<T>|null|undefined): T|null {
// ①AsyncPipeに初めてObservableが渡されたとき
if (!this._obj) {
if (obj) {
this._subscribe(obj);
}
return this._latestValue;
}
// (objの参照が変わった時(別のObservableが渡された時、今回の話をする上では重要ではない))
if (obj !== this._obj) {
this._dispose();
return this.transform(obj);
}
return this._latestValue;
}
// 略
}
这是在transform()函数内调用的重要代码。
class SubscribableStrategy implements SubscriptionStrategy {
createSubscription(async: Subscribable<any>, updateLatestValue: any): Unsubscribable {
return async.subscribe({
next: updateLatestValue,
error: (e: any) => {
throw e;
}
});
}
// 略
}
private _subscribe(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): void {
this._obj = obj;
this._strategy = this._selectStrategy(obj);
this._subscription = this._strategy.createSubscription(
obj, (value: Object) => this._updateLatestValue(obj, value));
}
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref!.markForCheck();
}
}
如果将getUsers()返回的Observable表示为?,
大致流程如下。
AsyncPipe
に?(=obj
)が渡されるnullnull2transform()
が呼ばれるnullnull3_obj===null
なので①のif文の内部へnullnull4_subscribe()
が呼ばれるnullnull5this._obj
を上書き?null6createSubscription
で?の購読を開始?null7return this._latestValue
?null目前为止,_latestValue仍然保持初始值null,未被覆盖,因此return this._latestValue将返回null。这就是所谓的”AsyncPipe初始值null问题”。
怎么打倒它
只需要一个选项:根据视频或文章中所示,我们可以通过使用诸如*ngIf或*ngFor等忽略空值的指令来实现击败它。
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';
@Component({
selector: 'app-root',
template: `
<ng-container *ngIf="users$ | async as users">
<user-list [users]="users"></user-list>
</ng-container>
`,
})
export class AppComponent implements OnInit {
readonly users$: Observavle<User[]>;
constructor(private userListService: UserListService) {
this.users$ = this.userListService.getUsers();
}
ngOnInit() {
}
}
应该始终通过Null安全的NgIf或NgFor来使用AsyncPipe。
这就是结论。
此外,在视频和下面的文章中,还介绍了一种称为Single State Stream的方法,可以将多个异步值捆绑成单个流并进行快照。请务必去看一看。
顺便提一句,在视频里
即使是像BehaviorSubject一样,已确定在订阅时流动值的流,也可能会出现null值。
因为您说过,所以我也进行了调查。
import { Component, OnInit } from '@angular/core';
import { UserListService } from '../user-list.service';
@Component({
selector: 'app-root',
template: `<user-list [users]="users$ | async"></user-list>`,
})
export class AppComponent implements OnInit {
readonly users$: Observavle<User[]>;
constructor(private userListService: UserListService) {
- this.users$ = this.userListService.getUsers();
+ this.users$ = this.userListService.users$;
}
ngOnInit() {
}
}
@Injectable({ providedIn: 'root' })
export class UserListService {
+ private _usersSubject = new BehaviorSubject<User[]>([]); // <-購読と同時に[]が流れる
+ get users$() {
+ return this._usersSubject.asObservable()
+ }
constructor(private http: HttpClient) { }
- getUsers(): Observable<User[]> {
- return this.http
- .get<{ data: User[] }>("https://reqres.in/api/users")
- .pipe(
- map(resp => resp.data)
- );
- }
+ getUsers(): void {
+ this.http
+ .get<{ data: User[] }>("https://reqres.in/api/users")
+ .pipe(map(resp => resp.data))
+ .subscribe(users => this._usersSubject.next(users));
+ }
}
然后,不会返回 null。
createSubscription
で?の購読を開始?null_updateLatestValue()
が呼び出される?nullthis._latestValue
を上書き?[]
markForCheck()
が呼ばれる?[]
7return this._latestValue
?[]
只要调用markForCheck(),可能会发生一些错误,因为我不清楚它会发生什么。但是,就最新的AsyncPipe而言,如果在订阅时同时流动值的是BehaviorSubject等等情况,似乎不会返回null。
如果有任何错误,请您指出,我将不胜感激?♂️。
明天是HKT100RTKN先生的日子!