Angular 2 Pass Async Data

本文的主要內容是介紹父子組件通訊時,如何傳遞異步的數據。咱們將經過一個具體的示例,來介紹不一樣的處理方式。假設咱們有一個博客組件,用來顯示博主信息和分類的帖子列表信息。具體以下圖所示:typescript

圖片描述

瞭解完具體需求後,接下來咱們來一步步實現該組件。json

Post Interfaces and Data

Post Interfaces

post.interface.tssegmentfault

// each post will have a title and category
export interface Post {
    title: string;
    category: string;
}

// grouped posts by category
export interface GroupPosts {
    category: string;
    posts: Post[];
}

Mock Posts Data

[
  { "title": "Functional Programming", "category": "RxJS" },
  { "title": "Angular 2 Component Inheritance", "category": "NG2" },
  { "title": "RxJS Operators", "category": "RxJS" },
  { "title": "Angular 2 Http Module - HTTP", "category": "WEB" },
  { "title": "RxJS Observable", "category": "RxJS" },
  { "title": "Angular 2 AsyncPipe", "category": "NG2" }
]

Blogger Component

posts.component.ts數組

import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Post, GroupPosts } from './post.interface';

@Component({
    selector: 'exe-posts',
    template: `
    <div class="list-group">
        <div *ngFor="let group of groupPosts;" class="list-group-item">
            <h4>{{ group.category }}</h4>
            <ul>
                <li *ngFor="let post of group.posts">
                    {{ post.title }}
                </li>
            </ul>
        <div>
    </div>
    `
})
export class PostsComponent implements OnInit, OnChanges {

    @Input()
    data: Post[];

    groupPosts: GroupPosts[];

    ngOnInit() {}

    ngOnChanges(changes: SimpleChanges) {
        console.dir(changes);
    }

    groupByCategory(data: Post[]): GroupPosts[] {
        if (!data) return;

        // 獲取目錄名,使用ES6中Set進行去重處理
        const categories = new Set(data.map(x => x.category));

        // 從新生成二維數組,用於頁面顯示
        const result = Array.from(categories).map(x => ({
            category: x,
            posts: data.filter(post => post.category === x)
        }));
        return result;
    }
}

blogger.component.ts瀏覽器

import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { Post } from './post.interface';

@Component({
    selector: 'exe-bloggers',
    template: `
        <h1>Posts by: {{ blogger }}</h1>
        <div>
            <exe-posts [data]="posts"></exe-posts>
        </div>
    `
})
export class BloggerComponent implements OnInit {

    blogger = 'Semlinker';
    posts: Post[];

    ngOnInit() {
        this.getPostsByBlogger()
            .subscribe(posts => this.posts = posts);
    }

    getPostsByBlogger(): Observable<Post[]> {
        return Observable.create((observer: Observer<Post[]>) => {
            setTimeout(() => {
                const posts = [
                    { "title": "Functional Programming", "category": "RxJS" },
                    { "title": "Angular 2 Component Inheritance", "category": "NG2" },
                    { "title": "RxJS Operators", "category": "RxJS" },
                    { "title": "Angular 2 Http Module - HTTP", "category": "WEB" },
                    { "title": "RxJS Observable", "category": "RxJS" },
                    { "title": "Angular 2 AsyncPipe", "category": "NG2" }
                ];
                observer.next(posts);
            }, 2000);
        })
    }
}

app.component.tsapp

import { Component } from '@angular/core';

@Component({
  selector: 'exe-app',
  template: `
    <exe-bloggers></exe-bloggers>
  `
})
export class AppComponent {}

以上代碼運行完後,你會發現瀏覽器中只顯示 Posts by: Semlinker 。那咱們要怎麼正確顯示帖子列表呢?異步

Solutions (方案)

Solution 1: Use *ngIf

使用 *ngIf 指令延遲 exe-posts 組件的初始化,須要調整的代碼以下:async

blogger.component.tspost

template: `
  <h1>Posts by: {{ blogger }}</h1>
  <div *ngIf="posts">
       <exe-posts [data]="posts"></exe-posts>
  </div>
`

posts.component.ts性能

ngOnInit() {
  this.groupPosts = this.groupByCategory(this.data);
}

Solution 2: Use ngOnChanges

當數據綁定輸入屬性的值發生變化的時候,Angular 將會主動調用 ngOnChanges() 方法。它會得到一個 SimpleChanges 對象,包含綁定屬性的新值和舊值,所以咱們能夠利用 ngOnChanges() 鉤子,執行 groupPosts 的數據初始化操做。須要調整的代碼以下:

1.移除 blogger.component.ts 組件模板中的 *ngIf 指令:

template: `
  <h1>Posts by: {{ blogger }}</h1>
  <div>
       <exe-posts [data]="posts"></exe-posts>
  </div>
`

2.更新 posts.component.ts

ngOnInit() {
  // this.groupPosts = this.groupByCategory(this.data);
}

ngOnChanges(changes: SimpleChanges) {
  if (changes['data']) {
     this.groupPosts = this.groupByCategory(this.data);
  }
}

Solution 3: Use RxJS BehaviorSubject

咱們也能夠利用 RxJS 中 BehaviorSubject 來監測變化。須要注意的是,在使用 Observable 或 Subject 時,在不使用的時候,須要及時取消訂閱,以免出現內存泄露的問題。具體代碼以下:

posts.component.ts

import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/takeWhile';
import { Post, GroupPosts } from './post.interface';

@Component({
    selector: 'exe-posts',
    template: `
    <div class="list-group">
        <div *ngFor="let group of groupPosts;" class="list-group-item">
            <h4>{{ group.category }}</h4>
            <ul>
                <li *ngFor="let post of group.posts">
                    {{ post.title }}
                </li>
            </ul>
        <div>
    </div>
    `
})
export class PostsComponent implements OnInit, OnChanges {

    private _data$ = new BehaviorSubject<Post[]>([]);

    @Input()
    set data(value: Post[]) {
        this._data$.next(value);
    }

    get data(): Post[] {
        return this._data$.getValue();
    }

    groupPosts: GroupPosts[];

    ngOnInit() {
        this._data$
            // 當this.groupPosts有值的時候,會自動取消訂閱
            .takeWhile(() => !this.groupPosts) 
            .subscribe(x => {
                this.groupPosts = this.groupByCategory(this.data);
            });
    }

    ngOnChanges(changes: SimpleChanges) { }

    groupByCategory(data: Post[]): GroupPosts[] {
        if (!data) return;

        // 獲取目錄名,使用ES6中Set進行去重處理
        const categories = new Set(data.map(x => x.category));

        // 從新生成二維數組,用於頁面顯示
        const result = Array.from(categories).map(x => ({
            category: x,
            posts: data.filter(post => post.category === x)
        }));
        return result;
    }
}

上面示例中,咱們使用了 RxJS 中 BehaviorSubject,若是想進一步瞭解詳細信息,請參考 - RxJS Subject

須要注意的是,若是使用 takeWhile 操做符,在頁面成功顯示後,若是輸入屬性又發生變化,頁面是不會隨着更新的,由於咱們已經取消訂閱了。若是數據源會持續變化,那麼能夠移除 takeWhile 操做符,而後 ngOnDestroy() 鉤子中執行取消訂閱操做。

Solution 4: Use Observable

除了上面提到的三種方案外,咱們還可使用 Observable 對象。即設置輸入屬性的類型是 Observable。具體示例以下:

posts.component.ts

import { Component, Input, OnChanges, OnInit, OnDestroy, SimpleChanges } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Post, GroupPosts } from './post.interface';

@Component({
    selector: 'exe-posts',
    template: `
    <div class="list-group">
        <div *ngFor="let group of groupPosts;" class="list-group-item">
            <h4>{{ group.category }}</h4>
            <ul>
                <li *ngFor="let post of group.posts">
                    {{ post.title }}
                </li>
            </ul>
        <div>
    </div>
    `
})
export class PostsComponent implements OnInit, OnChanges, OnDestroy {

    @Input() data: Observable<Post[]>; // 輸入屬性的類型是Observable

    groupPosts: GroupPosts[];

    dataSubscription: Subscription; // 用於組件銷燬時取消訂閱

    ngOnInit() {
        this.dataSubscription = this.data.subscribe(posts => {
            this.groupPosts = this.groupByCategory(posts);
        });
    }

    ngOnChanges(changes: SimpleChanges) { }

    ngOnDestroy() {
        this.dataSubscription.unsubscribe();
    }

    groupByCategory(data: Post[]): GroupPosts[] {
        if (!data) return;

        // 獲取目錄名,使用ES6中Set進行去重處理
        const categories = new Set(data.map(x => x.category));

        // 從新生成二維數組,用於頁面顯示
        const result = Array.from(categories).map(x => ({
            category: x,
            posts: data.filter(post => post.category === x)
        }));

        return result;
    }
}

blogger.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { Post } from './post.interface';

@Component({
    selector: 'exe-bloggers',
    template: `
        <h1>Posts by: {{ blogger }}</h1>
        <div>
            <exe-posts [data]="posts"></exe-posts>
        </div>
    `
})
export class BloggerComponent implements OnInit {

    blogger = 'Semlinker';

    posts: Observable<Post[]>;

    ngOnInit() {
        this.posts = this.getPostsByBlogger();
    }

    getPostsByBlogger(): Observable<Post[]> {
        return Observable.create((observer: Observer<Post[]>) => {
            setTimeout(() => {
                const posts = [
                    { "title": "Functional Programming", "category": "RxJS" },
                    { "title": "Angular 2 Component Inheritance", "category": "NG2" },
                    { "title": "RxJS Operators", "category": "RxJS" },
                    { "title": "Angular 2 Http Module - HTTP", "category": "WEB" },
                    { "title": "RxJS Observable", "category": "RxJS" },
                    { "title": "Angular 2 AsyncPipe", "category": "NG2" }
                ];
                observer.next(posts);
            }, 2000);
        })
    }
}

上面的示例是沒有在模板中直接使用 Observable 類型的輸入屬性,若須要在模板中直接使用的話,可使用 Angular 2 中的 AsyncPipe ,瞭解詳細信息,請查看 - Angular 2 AsyncPipe。另外在 Angular 2 Change Detection - 2 文章中,咱們也有介紹怎麼利用 Observable 提升 Angular 2 變化檢測的性能,有興趣的讀者能夠了解一下。

我有話說

1.上面介紹的方案,應該使用哪種?

這個仍是取決於實際的應用場景,若是數據源只會發生一次變化,那麼能夠考慮使用 *ngIf 指令。若是數據源會持續變化,能夠考慮使用其它的方案。

2.組件通訊還有哪些方式?

組件通訊的經常使用方式:@Input、@Output、@ViewChild、模板變量、MessageService、Broadcaster (Angular 1.x $rootScope 中 $on、$broadcast ) 等。

詳細的信息,請參考 - Angular 2 Components Communicate

參考資源

相關文章
相關標籤/搜索