你一定聽說過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》。如果你需要使用此譯文,請先聯繫我。