[Angular 大師之路] 認識 AsyncPipe (2) - 進階技巧

今天我們來認識一下兩個重要的 AsyncPipe 特性,可以幫助我們在使用 AsyncPipe 時更有信心,打造出更高效能的程式!

類型:觀念/技巧

難度:5 顆星

實用度:4 顆星

特性 1:自動退訂

先來看看這段簡單的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Component, OnInit, OnDestroy} from '@angular/core';
import { interval } from 'rxjs';
@Component({
selector: 'app-counter',
template: `{{ value }}`
})
export class CounterComponent implements OnInit, OnDestroy {
value = 0
ngOnInit() {
interval(1000).subscribe((counter) => {
console.log(counter);
this.value = counter;
})
}
ngOnDestroy() {
console.log('destroy');
}
}
@Component({
selector: 'my-app',
template: `
<app-counter *ngIf="display"></app-counter>
<button (click)="display = !display">Toggle</button>
`,
})
export class AppComponent {
display = true;
}

在上面的程式碼中,我們設計了 CounterComponent 並使用 RxJS 的 interval() 在訂閱後每秒變更一次資料,另外在畫面上設計一個按鈕來決定是否需要銷毀這個元件,當 displayfalse 時,<app-counter> 元件將會被銷毀,而當 displaytrue 時, <app-counter> 元件將重新產生。

看起來一切沒什麼問題,但是當我們打開 F12 時會發現,雖然元件被摧毀了,但 interval() 的行為並沒有停止!這將會造成每次產生元件時,就會產生一段新的 interval() ,當次數多了後將會佔據大量的記憶體,進而發生 memory leak 的問題;要避免這問題,最直覺的方式是當元件要被璀毀時,於 ngOnDestory 方法內使用 unsubscribe 取消訂閱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export class CounterComponent implements OnInit, OnDestroy {
value = 0
subscription: Subscription;
ngOnInit() {
this.subscription = interval(1000).subscribe((counter) => {
console.log(counter);
this.value = counter;
})
}
ngOnDestroy() {
console.log('destroy');
this.subscription.unsubscribe();
}
}

雖然取消訂閱人人有責,但是當程式中的 observable 越來越多時,總是會有不小心忘記訂閱的時候,這時 AsyncPipe 就能派上大作用啦!

使用 AsyncPipe 的自動退訂機制

AsyncPipe 的程式碼可以看到,當 AsyncPipe 處理 observable 時,會在 ngOnDestoy 時自動將 observable 退訂!因此上面的程式我們可以簡單改寫為:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component({
selector: 'app-counter',
template: `{{ value$ | async }}`
})
export class CounterComponent implements OnInit {
value$: Observable<number>;
ngOnInit() {
this.value$ = interval(1000).pipe(
tap(counter => console.log(counter))
);
}
}

由於 AsyncPipe 會自動退訂的關係,我們不再需要手動執行退訂的程式,整個程式看起來是不是清爽多啦!

特性 2:自動要求變更偵測

AsyncPipe 程式碼的另一角落,我們可以發現在資料變更時(不管是 Promise 還是 RxJS),會自動使用 ChangeDetectorRefmarkForCheck() 方法,自動要求變更偵測發生;會有這樣的程式需求也不難理解,當我們給予一個 observable 實體時,不管內部的值再怎麼變化,observable 的實體參考位置也不會變化,因此當元件的變更偵測策略為 OnPush 時,使用 AsyncPipe 就會發生沒有進行變更偵測的問題!所以 AsyncPipe 在訂閱(或呼叫 then())的同時,也會要求變更偵測需要處理!

搭配 OnPush 策略

透過上述提到的特性,如果元件中只剩下 observable + AsyncPipe 時,我們就可以光明正大地把元件的 OnPush 策略打開,並且完全不用手動去呼叫 markForCheck 方法,AsyncPipe 會在需要變更偵測時主動幫我們處理!

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-counter',
template: `{{ (data$ | async)?.value }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
@Input() data$: Observable<any>;
ngOnInit() { }
}

從此以後每個單純用來顯示資料的元件,再打開 OnPush 之後,既能夠維持元件一定程度的高效能,又不怕忘記呼叫 markForCheck 啦!

善用 RxJS 與 AsyncPipe,要打造出既好維護,相對效能又高的元件一點都不困難啊!!

今天的程式碼參考連結:

https://stackblitz.com/edit/ironman2019-asyncpipe-onpush?file=src/app/app.component.ts