Async Pipe可以对 Angular 应用程序的更改检测策略产生巨大影响。如果到目前为止您还感到困惑,请详解读完全文。我们一起来了解一下吧!
在 Angular 中,Async Pipe本质上是执行以下三个任务的管道:
- 它订阅一个observable或一个Promise并返回最后发出的值。
- 每当发出新值时,它都会标记组件为需要要检查的。这意味着Angular将在下一个周期中为该组件运行Change Detector。
- 当组件被销毁时,它会取消订阅可观察的内容。
此外,作为最佳实践,建议尝试使用 onPush 更改检测策略上的组件和异步管道来订阅可观察对象。
如果您是Angular的初学者,也许上面对异步管道的解释让人不知所措。因此,在本文中,我们将尝试使用代码示例逐步理解异步管道。只需创建一个新的Angular 项目并继续操作即可;在文章的最后,您应该对异步管道有一些深刻的了解。
1. 创建服务
让我们从创建产品接口和服务开始。
1 2 3 4 5 6 7 8 9 10
| export interface IProduct {
Id : string; Title : string; Price : number; inStock : boolean;
}
|
创建IProduct接口后,接下来在 Angular 服务内部创建一个IProduct数组来执行读写操作。
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
| import { Injectable } from '@angular/core'; import { IProduct } from './product.entity';
@Injectable({ providedIn: 'root' }) export class AppService {
Products : IProduct[] = [ { Id:"1", Title:"Pen", Price: 100, inStock: true }, { Id:"2", Title:"Pencil", Price: 200, inStock: false }, { Id:"3", Title:"Book", Price: 500, inStock: true } ]
constructor() { } }
|
请记住,在实际应用程序中,我们从远程API获取数据;然而,在这里我们模仿本地数组中的读取和写入操作,以重点关注异步管道。
为了执行读写操作,我们将 Products 数组包装在 a 中BehaviorSubject,并在每次将新项目推送到数组时发出一个新数组Products。
为此,请在服务中添加代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11
| Products$ : BehaviorSubject<IProduct[]>; constructor() { this.Products$ = new BehaviorSubject<IProduct[]>(this.Products); } AddProduct(p: IProduct): void{ this.Products.push(p); this.Products$.next(this.Products); }
|
让我们看一下代码:
- BehaviourSubject是一种发出默认值或最后发出值的Subject。我们使用的BehaviorSubject最初发送的默认值是Products数组。
- 在该AddProduct方法中,我们接收一个Product并将其推送到数组中。
- 在该AddProduct方法中,将接收到的Product推送到Products数组后,我们将发出更新后的Products数组。
目前,该服务已准备就绪。接下来,我们将创建两个组件: 一个用于添加产品,另一个用于在表格上显示所有产品。
2. 添加产品
创建一个名为AddProduct的组件,并添加一个响应式式表单来接受产品信息。
1 2 3 4 5 6 7 8 9 10 11
| productForm: FormGroup; constructor(private fb: FormBuilder, private appService: AppService) { this.productForm = this.fb.group({ Id: ["", Validators.required], Title: ["", Validators.required], Price: [], inStock: [] }) }
|
我们使用FormBuilder来创建FormGroup组件,并在template中使用HTML表单结合productForm来接受用户输入,如下所示:
1 2 3 4 5 6 7
| <form (ngSubmit)='addProduct()' [formGroup]='productForm'> <input formControlName='Id' type="text" class="form-control" placeholder="Enter ID" /> <input formControlName='Title' type="text" class="form-control" placeholder="Enter Title" /> <input formControlName='Price' type="text" class="form-control" placeholder="Enter Price" /> <input formControlName='inStock' type="text" class="form-control" placeholder="Enter Stock " /> <button [disabled]='productForm.invalid' class="btn btn-default">Add Product</button> </form>
|
在函数AddProduct中,我们将检查表单是否有效。如果有效,我们调用该服务将一种产品推送到Products数组。该AddProduct函数应如下所示:
1 2 3 4 5
| addProduct() { if (this.productForm.valid) { this.appService.AddProduct(this.productForm.value); } }
|
到目前为止,我们已经创建了一个包含reactive form的组件,用于输入产品信息并调用服务在 Products 数组中插入新产品。如果您使用过 Angular,上面的代码应该很简单。
3. 显示产品列表
为了显示产品列表, 我需要这样做:
- 将组件的更改检测策略设置为 Default。
- 在组件中注入AppService。
- 使用 subscribe 方法从 observable 中获取数据。
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
| @Component({ selector: 'app-list-products', templateUrl: './list-products.component.html', styleUrls: ['./list-products.component.css'], changeDetection: ChangeDetectionStrategy.Default }) export class ListProductsComponent implements OnInit, OnDestroy {
products: IProduct[] = [] productSubscription?: Subscription constructor(private appService: AppService) { }
productObserver = { next: (data: IProduct[]) => { this.products = data; }, error: (error: any) => { console.log(error) }, complete: () => { console.log('product stream completed ') } }
ngOnInit(): void { this.productSubscription = this.appService.Products$.subscribe(this.productObserver) }
ngOnDestroy(): void { if (this.productSubscription) { this.productSubscription.unsubscribe(); } } }
|
让我们看一下代码:
- products变量保存从服务返回的数组。
- 是productSubscription 是RxJS Subscription类型的变量,用于保存从可观察对象的订阅方法返回的订阅。
- 这productObserver是一个具有 next、error 和complete 回调函数的对象。
- 观察者productObserver被传递给 subscribe 方法。
- 在ngOnDestrory()生命周期钩子中,我们取消订阅可观察的内容。
在html模板上,您可以在表格中显示产品,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <table> <thead> <tr> <th>Id</th> <th>Title</th> <th>Price</th> <th>inStock</th> </tr> </thead> <tbody> <tr *ngFor="let p of products"> <td>{{p.Id}}</td> <td>{{p.Title}}</td> <td>{{p.Price}}</td> <td>{{p.inStock}}</td> </tr> </tbody> </table>
|
4. 使用组件
我们将使用这两个组件作为同级组件,如下所示。
1 2 3 4 5 6 7 8
| <h1>{{title}}</h1>
<app-add-product></app-add-product>
<hr/> <app-list-products></app-list-products>
|
这里您应该注意的一个关键点是组件AddProduct和ListProducts组件是不相关的。它们之间只有两种方式传递数据:
- 通过父组件进行通信
- 通过使用服务进行通信
我们已经创建了一项服务,并将使用该服务在这两个组件之间传递产品信息。
5. 运行应用程序
您可以通过单击“添加产品”按钮来添加产品。这会调用服务中的一个函数,该函数会更新数组并从可观察对象中发出更新后的数组。
列出产品的组件会订阅可观察的内容,因此每当我们添加另一个项目时,表就会更新。到目前为止,一切都很好。
6. 使用 onPush 变化检测策略
如果您还记得ListProducts组件更改检测策略设置为默认值。现在让我们继续将策略更改为onPush:
1 2 3 4 5 6 7 8 9
| @Component({ selector: 'app-list-products', templateUrl: './list-products.component.html', styleUrls: ['./list-products.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ListProductsComponent implements OnInit, OnDestroy {
|
再次,继续运行该应用程序。你发现了什么?正如您注意到的那样,当您用AddProduct组件添加产品时,它会被添加到数组中,甚至更新的数组也会从服务中发出。尽管如此,该 ListProducts组件仍未更新。发生这种情况是因为ListProducts组件的更改检测策略设置为onPush。
将更改检测策略更改为 onPush 可防止表被新产品刷新。
对于具有onPush更改检测策略的组件,Angular仅在新引用传递给组件时才运行更改检测器。但是,当observable发出新元素时,它仍然是原来的引用。因此,Angular没有运行变更检测器,并且更新的 Products 数组也没有显示在组件中。
您可以在此处了解有关Angular变化检测器的更多信息。
7. 我们该如何解决这个问题?
我们可以通过手动调用更改检测器来解决此问题。为此,请注入ChangeDetectorRef组件并调用该markForCheck()方法。
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
| export class ListProductsComponent implements OnInit, OnDestroy {
products: IProduct[] = [] productSubscription?: Subscription constructor(private appService: AppService, private cd: ChangeDetectorRef) {
}
productObserver = { next: (data: IProduct[]) => { this.products = data; this.cd.markForCheck(); }, error: (error: any) => { console.log(error) }, complete: () => { console.log('product stream completed ') } } ngOnInit(): void { this.productSubscription = this.appService.Products$.subscribe(this.productObserver) }
ngOnDestroy(): void { if (this.productSubscription) { this.productSubscription.unsubscribe(); } } }
|
至此,我们完成了以下任务:
我们将 Angular ChangeDetectorRef 注入到组件中。
该markForCheck()方法将该组件及其所有父组件标记为dirty组件,以便 Angular 在下一个更改检测周期中检查更改。
现在运行应用程序,您应该能够看到更新的产品数组。
8. Subscribe方式分析
正如您所看到的,在设置为 的组件中onPush,要使用可观察量,请按照以下步骤操作。
- 订阅可观察的内容。
- 手动运行更改检测。
- 取消订阅可观察的内容。
该方法的优点subscribe()是:
- 属性可以在模板中的多个位置使用。
- 属性可以用在组件类的不同位置。
- 您可以在订阅可观察对象时运行自定义业务逻辑。
一些缺点是:
- 对于onPush变更检测策略,您必须手动标记组件以使用该markForCheck方法运行变更检测器。
- 您必须明确取消订阅可观察量。
- 当组件中使用许多可观察量时,这种方法可能会失控。如果我们错过取消订阅任何可观察的内容,则可能存在潜在的内存泄漏等。
使用Async Pipe可以解决上述问题。
9. Async管道
异步管道是在组件中处理可观察对象的更好且更推荐的方式。在底层,异步管道执行以下三项任务:
- 它订阅可观察对象发出的最后值。
- 当发出新值时,它标记要检查更改的组件。
- 当组件被销毁时,异步管道会自动取消订阅,以避免潜在的内存泄漏。
所以基本上,异步管道会完成您为订阅方法手动执行的所有三项任务。
让我们修改ListProducts组件以使用异步管道。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Component({ selector: 'app-list-products', templateUrl: './list-products.component.html', styleUrls: ['./list-products.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ListProductsComponent implements OnInit {
products?: Observable<IProduct[]>; constructor(private appService: AppService) {} ngOnInit(): void { this.products = this.appService.Products$; } }
|
我们删除了ListProductsComponent之前的所有代码,并将服务返回的可观察赋给产品变量。在现在HTML模板上,使用异步管道。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <table> <thead> <tr> <th>Id</th> <th>Title</th> <th>Price</th> <th>inStock</th> </tr> </thead> <tbody> <tr *ngFor="let p of products | async"> <td>{{p.Id}}</td> <td>{{p.Title}}</td> <td>{{p.Price}}</td> <td>{{p.inStock}}</td> </tr> </tbody> </table>
|
使用异步管道可以使代码更清晰,并且您不需要为onPush更改检测策略手动运行更改检测器。在应用程序上,您会看到ListProducts每当添加新产品时组件都会重新。
始终建议的最佳做法是:
- 将组件变化检测策略设置为onPush
- 使用异步管道来处理可观察量
我希望您觉得这篇文章很有用,并且现在已经准备好在您的 Angular 项目中使用异步管道。
10. 如何处理异常
如果程序在抓取数据时出现异常, 此时我们可以在 tap算子中捕捉异常, 在catchError算子中将error转换成空数组. 在页面向用户显示异常信息.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component({ selector: 'app-list-products', templateUrl: './list-products.component.html', styleUrls: ['./list-products.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ListProductsComponent implements OnInit {
products?: Observable<IProduct[]>; error: Error | null = null; constructor(private appService: AppService) {} ngOnInit(): void { this.products = this.appService.Products$.pipe( tap({ error: (error) => this.error = error }) catchError((err) => of([])) ); } }
|
在页面检测是否有错误发生, 将错误信息显示给用户.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <div *ngIf="error" class="error">{{error}}</div> <table> <thead> <tr> <th>Id</th> <th>Title</th> <th>Price</th> <th>inStock</th> </tr> </thead> <tbody> <tr *ngFor="let p of products | async"> <td>{{p.Id}}</td> <td>{{p.Title}}</td> <td>{{p.Price}}</td> <td>{{p.inStock}}</td> </tr> </tbody> </table>
|
11. Angular 系列文章
最新更新以及更多Angular相关文章请访问 Angular合集 | 鹏叔的技术博客
12. 参考文档
Angular Basics: Step-by-Step Understanding the Async Pipe
How to Handle Errors Reactively when Using the Async Pipe