本文的主要内容是介绍父子组件通信时,如何传递异步的数据。我们将通过一个具体的示例,来介绍不同的处理方式。假设我们有一个博客组件,用来显示博主信息和分类的帖子列表信息。具体如下图所示:
了解完具体需求后,接下来我们来一步步实现该组件。
Post Interfaces and Data
Post Interfaces
post.interface.ts
// 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.ts
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 组件的初始化,需要调整的代码如下:
blogger.component.ts
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。