学习Angular的AsyncPipe

这篇文章是2022年Angular圣诞日历上第20天的文章。
这是关于学习AsyncPipe的记录。

异步管道是什么?

首先,我們先確認官方網頁。

 

异步管道订阅 Observable 或 Promise,并返回其已发出的最新值。当有新值被发出时,异步管道标记该组件以进行变更检查。当组件被销毁时,异步管道会自动取消订阅,以避免潜在的内存泄漏。当表达式的引用发生变化时,异步管道会自动取消订阅旧的 Observable 或 Promise,并订阅新的。

    1. 通过订阅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表示为?,
大致流程如下。

No処理_obj_latestValue1AsyncPipeに?(=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。

No処理_obj_latestValue6createSubscriptionで?の購読を開始?null
_updateLatestValue()が呼び出される?null
this._latestValueを上書き?[]
markForCheck()が呼ばれる?[]7return this._latestValue?[]

只要调用markForCheck(),可能会发生一些错误,因为我不清楚它会发生什么。但是,就最新的AsyncPipe而言,如果在订阅时同时流动值的是BehaviorSubject等等情况,似乎不会返回null。

如果有任何错误,请您指出,我将不胜感激?‍♂️。

明天是HKT100RTKN先生的日子!

广告
将在 10 秒后关闭
bannerAds