你一定听说过Angular的AsyncPipe
管道吧?这是一个很方便的管道,我们可以在模版里面使用它,这样就不用强制性的去从Observables或Promises处理展开数据。AsyncPipe
有一些我们刚开始可能看不出来的魔法力十足的东西。本文中我们希望能对这个有用的小工具的内部运作有更加深刻的了解。
订阅长连接的数据流
一般我们想起来AsyncPipe
,只会想起解析来自Http请求的值。我们发起一个Http请求,返回一个Observable<Response>
,做一些数据转化(比如map(...)
,filter(...)
),最终向我们的组件模版提供一个Observable。下面的代码展示了通常这种用法是什么样子:
...
@Component({
...
template: `
<md-list>
<a md-list-item
*ngFor="let contact of contacts | async"
title="View {{contact.name}} details">
<img md-list-avatar [src]="contact.image" alt="Picture of {{contact.name}}">
<h3 md-line>{{contact.name}}</h3>
</a>
</md-list>`,
})
export class ContactsListComponent implements OnInit {
contacts: Observable<Array<Contact>>;
constructor(private contactsService: ContactsService) {}
ngOnInit () {
this.contacts = this.contactsService.getContacts();
}
}
我们可以将上面这种场景的Observable称之为short-lived
。这个Observable只发一个值,在上面的例子里就是一个contact数组,然后Observable就会结束。当我们使用http的时候这是很典型的场景,特别是当使用Promises的时候这基本是唯一的场景。
但是,我们完全可以拥有发出多个值的Observable。例如使用websockets。我们可能会随着时间的推移建立一个数组。
让我们模拟一个可发射数字类型数组的Observable。每当一个新的数据被添加到数组时,它都会发射一个数组,而不是一次性的发射一个数组就完事了。为了不让数组无限增长,我们将其限制为最后五个数据。
...
@Component({
selector: 'my-app',
template: `
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`
})
export class AppComponent {
items = Observable.interval(100)
.scan((acc, cur)=>[cur, ...acc].slice(0, 5), []);
}
看到了吧,借助AsyncPipe
,我们的列表可以很简便的保持良好的同步。
自动跟踪引用
让我们回到上面的代码,把它改成不使用AsyncPipe
的样子。同时我们使用一个按钮来重新生成数字,在每次重新生成队列时,为元素选一个随机的背景色。
...
@Component({
selector: 'my-app',
template: `
<button (click)="newSeq()">New random sequence</button>
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items">{{item.num}}</li>
</ul>`
})
export class AppComponent {
items = [];
constructor () {
this.newSeq();
}
newSeq() {
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), [])
.subscribe(items => this.items = items);
}
}
让我们重构代码以跟踪订阅,并在每次创建新的Observable时将其删除。
...
export class AppComponent {
...
subscription: Subscription;
newSeq() {
if (this.subscription) {
this.subscription.unsubscribe();
}
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
this.subscription = Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), [])
.subscribe(items => this.items = items);
}
}
每次我们订阅Observable时,我们都把这个订阅保存在组件的实例成员中。 然后,当我们再次运行newSeq
时,我们检查是否存在需要unsubscribe
的订阅。 因此,无论我们多久点击一次按钮,我们看不到列表在不同颜色之间切换。
让我们再次更改ngFor
来使用AsyncPipe
,并去掉所有手动的追踪。
@Component({
selector: 'my-app',
template: `
<button (click)="newSeq()">New random sequence</button>
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items | async">{{item.num}}</li>
</ul>`
})
export class AppComponent {
items: Observable<any>;
constructor () {
this.newSeq();
}
newSeq() {
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
this.items = Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), []);
}
}
你肯定知道一旦组件被销毁,AsyncPipe将从Observables退订。但是,你是否还知道一旦表达式的引用发生更改,它就会退订?是的,一旦我们为this.items
分配了一个新的Observable,AsyncPipe
将自动取消订阅先前绑定的Observable! 这不仅使我们的代码美观整洁,而且还防止了非常细微的内存泄漏的发生。
标记变更检测
现在我将为你介绍AsyncPipe
最后一个巧妙的功能。如果你已经阅读了文章Angular’s change detection,那么你肯定知道可以使用OnPush
策略进一步加快Angular变更检测的速度。让我们重构示例代码,并引入一个SeqComponent
来显示数组,而我们的根组件将管理数据并通过input传递数据。
直接创建SeqComponent
:
@Component({
selector: 'my-seq',
template: `
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items">{{item.num}}</li>
</ul>`
})
export class SeqComponent {
@Input()
items: Array<any>;
}
@Input()
修饰符意味着组件将通过属性绑定从外部接收数据。
我们的根组件维护一个数组seqs
,并通过单击按钮将新的Long-lived
的Observable推入其中。 它使用*ngFor将每个Observable传递给新的SeqComponent实例。看下面的代码,我们在属性绑定表达式([items] =“seq|async”)
中使用AsyncPipe传递纯数组,而不是Observable,因为这就是SeqComponent想要的效果。
@Component({
selector: 'my-app',
template: `
<button (click)="newSeq()">New random sequence</button>
<ul>
<my-seq *ngFor="let seq of seqs" [items]="seq | async"></my-seq>
</ul>`
})
export class AppComponent {
seqs = [];
constructor () {
this.newSeq();
}
newSeq() {
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
this.seqs.push(Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), []));
}
}
到目前为止,我们尚未对基础的变更检测策略进行更改。如果你多次单击按钮,我们会获得多个列表,这些列表在不同的时间独立更新。
但是,就变更检测而言,这意味着每次任意一个Observables发射数据,所有的组件都会被检查。这非常浪费资源。通过将SeqComponent
的更改检测方式设置为OnPush
,我们可以做得更好,这意味着它仅在输入数据(在我们的情况下为数组)发生更改的情况下才检查其绑定。
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'my-seq',
...
})
这样看起来似乎行得通。但是问题来了:它之所以能起作用是因为我们的Observable每次发射新值时都会创建一个全新的数组。这样实际上在大多数情况下是没问题的。但让我们考虑一下使用一种不同的实现方式,只改变现有数组,而不是每次都重新创建它。
Observable.interval(1000)
.scan((acc, num)=>{
acc.splice(0, 0, {num, color});
if (acc.length > 5) {
acc.pop()
}
return acc;
}, [])
如果我们这样做,OnPush似乎不再起作用,因为items
的引用不再改变。实际上,当我们这样做时,我们发现每个列表都只会有一个元素。
让我们更改SeqComponent
,使其采用Observable而不是数组作为输入。
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'my-seq',
template: `
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items | async">{{item.num}}</li>
</ul>`
})
export class SeqComponent {
@Input()
items: Observable<Array<any>>;
}
请注意,由于它不再处理普通数组,因此现在在其模板中使用了AsyncPipe
。
我们还需要更改AppComponent
,属性绑定中不再使用AsyncPipe
。
<ul>
<my-seq *ngFor="let seq of seqs" [items]="seq"></my-seq>
</ul>
回顾一下,我们的数组实例不变,我们的Observable实例也不变。那么,为什么在这种情况下OnPush
可以工作?原因可以在AsyncPipe
本身的源代码中找到。
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
AsyncPipe
标记了要检查的组件的ChangeDetectorRef
,迅速告诉更改检测机制此组件可能要有更改。如果你想更详细地了解其工作原理,建议阅读 in-depth change detection article。
结语
我们通常将AsyncPipe
视为一个巧妙的小工具,可以在我们的组件中节省几行代码。 但是实际上,它为管理异步任务省去了很多复杂性。它真的非常重要,价值万金。
本文由我翻译自Christoph Burgdorf的文章《Three things you didn’t know about the AsyncPipe》。如果你需要使用此译文,请先联系我。