Angular Async Pipe详解

Async Pipe可以对 Angular 应用程序的更改检测策略产生巨大影响。如果到目前为止您还感到困惑,请详解读完全文。我们一起来了解一下吧!

在 Angular 中,Async Pipe本质上是执行以下三个任务的管道:

  • 它订阅一个observable或一个Promise并返回最后发出的值。
  • 每当发出新值时,它都会标记组件为需要要检查的。这意味着Angular将在下一个周期中为该组件运行Change Detector。
  • 当组件被销毁时,它会取消订阅可观察的内容。

此外,作为最佳实践,建议尝试使用 onPush 更改检测策略上的组件和异步管道来订阅可观察对象。

如果您是Angular的初学者,也许上面对异步管道的解释让人不知所措。因此,在本文中,我们将尝试使用代码示例逐步理解异步管道。只需创建一个新的Angular 项目并继续操作即可;在文章的最后,您应该对异步管道有一些深刻的了解。

1. 创建服务

让我们从创建产品接口和服务开始。


export interface IProduct {

     Id : string; 
     Title : string; 
     Price : number; 
     inStock : boolean;

}

创建IProduct接口后,接下来在 Angular 服务内部创建一个IProduct数组来执行读写操作。

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。

为此,请在服务中添加代码,如下所示:


Products$ : BehaviorSubject<IProduct[]>; 
  constructor() {
    this.Products$ = new BehaviorSubject<IProduct[]>(this.Products);
   }
  
   AddProduct(p: IProduct): void{
    this.Products.push(p);
    this.Products$.next(this.Products);
   }

让我们看一下代码:

  1. BehaviourSubject是一种发出默认值或最后发出值的Subject。我们使用的BehaviorSubject最初发送的默认值是Products数组。
  2. 在该AddProduct方法中,我们接收一个Product并将其推送到数组中。
  3. 在该AddProduct方法中,将接收到的Product推送到Products数组后,我们将发出更新后的Products数组。

目前,该服务已准备就绪。接下来,我们将创建两个组件: 一个用于添加产品,另一个用于在表格上显示所有产品。

2. 添加产品

创建一个名为AddProduct的组件,并添加一个响应式式表单来接受产品信息。


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来接受用户输入,如下所示:

<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函数应如下所示:

addProduct() {
    if (this.productForm.valid) {
      this.appService.AddProduct(this.productForm.value);
    }
  }

到目前为止,我们已经创建了一个包含reactive form的组件,用于输入产品信息并调用服务在 Products 数组中插入新产品。如果您使用过 Angular,上面的代码应该很简单。

3. 显示产品列表

为了显示产品列表, 我需要这样做:

  • 将组件的更改检测策略设置为 Default。
  • 在组件中注入AppService。
  • 使用 subscribe 方法从 observable 中获取数据。

@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模板上,您可以在表格中显示产品,如下所示:


<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. 使用组件

我们将使用这两个组件作为同级组件,如下所示。


<h1>{{title}}</h1>

<app-add-product></app-add-product>

<hr/>
<app-list-products></app-list-products>

这里您应该注意的一个关键点是组件AddProduct和ListProducts组件是不相关的。它们之间只有两种方式传递数据:

  1. 通过父组件进行通信
  2. 通过使用服务进行通信

我们已经创建了一项服务,并将使用该服务在这两个组件之间传递产品信息。

5. 运行应用程序

您可以通过单击“添加产品”按钮来添加产品。这会调用服务中的一个函数,该函数会更新数组并从可观察对象中发出更新后的数组。

列出产品的组件会订阅可观察的内容,因此每当我们添加另一个项目时,表就会更新。到目前为止,一切都很好。

6. 使用 onPush 变化检测策略

如果您还记得ListProducts组件更改检测策略设置为默认值。现在让我们继续将策略更改为onPush:


@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()方法。

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()方法将该组件及其所有父组件标记为脏组件,以便 Angular 在下一个更改检测周期中检查更改。

现在运行应用程序,您应该能够看到更新的产品数组。

8. Subscribe方式分析

正如您所看到的,在设置为 的组件中onPush,要使用可观察量,请按照以下步骤操作。

  • 订阅可观察的内容。
  • 手动运行更改检测。
  • 取消订阅可观察的内容。

该方法的优点subscribe()是:

  • 属性可以在模板中的多个位置使用。
  • 属性可以用在组件类的不同位置。
  • 您可以在订阅可观察对象时运行自定义业务逻辑。

一些缺点是:

  • 对于onPush变更检测策略,您必须手动标记组件以使用该markForCheck方法运行变更检测器。
  • 您必须明确取消订阅可观察量。
  • 当组件中使用许多可观察量时,这种方法可能会失控。如果我们错过取消订阅任何可观察的内容,则可能存在潜在的内存泄漏等。

使用Async Pipe可以解决上述问题。

9. Async管道

异步管道是在组件中处理可观察对象的更好且更推荐的方式。在底层,异步管道执行以下三项任务:

  • 它订阅可观察对象发出的最后值。
  • 当发出新值时,它标记要检查更改的组件。
  • 当组件被销毁时,异步管道会自动取消订阅,以避免潜在的内存泄漏。

所以基本上,异步管道会完成您为订阅方法手动执行的所有三项任务。

让我们修改ListProducts组件以使用异步管道。

@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模板上,使用异步管道。

<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转换成空数组. 在页面向用户显示异常信息.


@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([]))
  );
  }
}

在页面检测是否有错误发生, 将错误信息显示给用户.

 <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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容

  • 原文来源翻译说明: 翻译已取得作者授权。中英对照, 意译, 译文会有适当的排版。翻译不到位的地方,还请看官多多指教...
    萧哈哈阅读 561评论 0 1
  • 基础知识篇 1.AngularJs与Angular区别 AngularJs 建筑模型:MVC 推荐语言:javas...
    TedFan阅读 1,237评论 0 0
  • 文章翻译自 A Comprehensive Guide to Angular onPush Change Dete...
    daozun阅读 701评论 0 1
  • 1. 在哪个目录中,所有外部模块和文件都存储在Angular 4中? Angular4将所有外部模块和文件存储在n...
    没糖_cristalle阅读 282评论 0 0
  • 看到Observable和RxJS就感觉很亲切,因为之前做Android开发的时候接触过RxJava。Obs...
    tuacy阅读 5,611评论 0 5