.

iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 44

Day 44 - toSignal 函數的 manualCleanup

  • 分享至 

  • xImage
  •  

toSignal 函數在 root-level 服務中建立訊號並將其傳回給元件時,creation context 就是 root-level 服務。當元件被銷毀時,root-level 服務,並不清理 Observable。因此,訊號未被取消訂閱,且應用程式存在記憶體洩漏 (memory leak)。

有使用 toSignal 的建議:

  • 在元件上呼叫 toSignal 函數,以便 creation context 是該元件。當元件被銷毀時,它會清理 Observable 並取消訂閱訊號。
  • 將元件的 injector 傳遞給 toSignal 函數,以便 injector 的 DestroyRef 在元件被銷毀時清理 Observable。
  • 使用在自動清理的 Observables,訊號自動取消訂閱。
  • 請不要在 root injector建立服務並在元件層級提供它。當元件被銷毀時,服務也會被銷毀。該服務清理 Observables 並取消訂閱 toSignal 函數創建的任何訊號。

如果我們不想將責任委託給 creation context 或 injector,我們將指定 manualCleanup 選項為 true 並手動取消訂閱訊號。

在大多數情況下,使用 DestroyRef 清理 Observable 並取消訂閱訊號的預設行為就足夠了。 當其他一切都不適用時,請使用 manualCleanup 選項。

interface ToSignalOptions<T> {
 manualCleanup?: boolean | undefined;
}

我示範了一個記憶體洩漏的範例,其中指定了 manualCleanupinjector

此外,我還示範了手動取消訂閱 toSignal 函數建立的訊號的三個範例。

  • 使用 takeUntil RxJS 運算子在按下某個鍵時清理 Observable。
  • 使用 takeUntilDestroyed(destroyRef$) 和元件的 DestroyRef 執行回呼來清理 Observable。
  • 此元件注入一個 DestroyRef 並在建構函式中定義 DestroyRef.OnDestroy。當元件被銷毀時,DestroyRef.OnDestroy 會執行回呼以向Subject 發出一個值並清理 Observable。

應用程式路由

export const routes: Route[] = [
 {
   path: 'a',
   loadComponent: () => import('./a.component')
 },
 {
   path: 'b',
   loadComponent: () => import('./b.component')
 },
 {
   path: '',
   pathMatch: 'full',
   redirectTo: 'b',
 },
 {
   path: '**',
   redirectTo: 'b',
 }
]

此示範有一些路由到 AComponent (/a) 和 BComponent (/b)。

當 manualCleanup 和 injector 選項存在時記憶體洩漏

@Injectable({ providedIn: 'root' })
export class ManualCleanupService {
 createTimer2Signal(injector: Injector) {
   const timer2$ = timer(0, 1000).pipe(
     tap((v) => console.log(`#timer2 ->`, v)),
   );

   return toSignal(timer2$, { manualCleanup: true, injector });
 }
}

ManualCleanupService 服務是一個 root-level 服務,它聲明了一個接受 injector 的 ceateTimer2Signal 方法。此方法呼叫每秒發出一個數字的 toSignal 函數。 toSignal 選項由 manualCleanup 屬性和 injector 組成。

<p>Memory leak manualCleanup Signal: {{ memoryLeakSignal() }}</p>
export default class AComponent {
 service = inject(ManualCleanupService);
 injector = inject(Injector);

 memoryLeakSignal = this.service.createTimer2Signal(this.injector);
}

AComponent 被銷毀後,memoryLeakSignal 會導致記憶體洩漏。即使組件的 injector 被傳遞給 createTimer2Signal 方法,當 manualCleanup 等於 true 時,訊號也會忽略它。當 AComponent 被銷毀時,timer2$ 不會被取消訂閱。 當路由變更後創建 AComponent 時,它會使用新的 timer2$ 建立一個新的 memoryLeakSignal,並且前一個仍然發出數字。開啟 Chrome Dev Console,舊的和新的 timer2$ 繼續 console log。

manualCleanup 的範例

範例1:使用takeUntil 清理 Observable

@Injectable({ providedIn: 'root' })
export class ManualCleanupService {
 #document = inject(DOCUMENT);
 #keyup$ = fromEvent(this.#document, 'keyup');

 #timer$ = timer(0, 1000).pipe(
   tap((v) => console.log(`#timer$ ->`, v)),
   takeUntil(this.#keyup$),
 );

 manualCleanupTimerSignal = toSignal(this.#timer$, { manualCleanup: true });
}

#timer$ 是一個每秒發出一個數字的 Obervable。 當使用者按下一個鍵時,Observable 就會清理。manualCleanupTimerSignal 呼叫 toSignal 函數來建立帶有 manualCleanup 選項的訊號。

@Component({
 selector: 'app-b',
 template: `
   <p>Manual Cleanup Timer Signal: {{ timerSignal() }}</p>
 `,
})
export default class BComponent {
 service = inject(ManualCleanupService);
 timerSignal = this.service.manualCleanupTimerSignal;
}

當使用者按下任何按鍵時,Observable 就會清理,timerSignal 會顯示 BComponent 元件中最後一次成功的值。

@Component({
 selector: 'app-a',
 template: `<p>timerSignal: {{ timerSignal() }}</p>`,
})
export default class AComponent {
 service = inject(ManualCleanupService);
 timerSignal = this.service.manualCleanupTimerSignal;
}

當使用者路由到 AComponent 元件時,元件會顯示 timerSignal 訊號最後一次成功的值。當使用者開啟 Chrome Dev Console 時,#timer$ 沒有新的 console log。

範例2:使用 takeUntilDestroyed 清理 Observable

export default class AComponent {
 injector = inject(Injector);
 destroyRef$ = this.injector.get(DestroyRef)

 #timer3$ = timer(0, 1000).pipe(
   tap((v) => console.log(`#timer3 ->`, v)),
   takeUntilDestroyed(this.destroyRef$),
 );
 takeUntilDestroyedSignal = toSignal(this.#timer3$, { manualCleanup: true });
}
<p>takeUntilDestroyed Signal: {{ takeUntilDestroyedSignal() }}</p>

#timer3$ 是一個每秒發出一個數字的 Obervable。 當使用者路由到 BComponent 元件時,AComponent 被銷毀,而 takeUntilDestroyed RxJS 運算子使用 DestroyRef 來清理 Observable。

當使用者再次路由到 AComponent 元件時,該元件會建立一個新訊號和一個從 0 開始的新計時器。當使用者開啟 Chrome Dev Console 時,先前的 #timer3$ 不會 console log,但新的 #timer3$ 會 console log。

#timer3# 沒有 takeUntilDestroyed 時,會導致 AComponent 被銷毀時出現記憶體洩漏。這是因為 manualCleanup 選項忽略了 creation context(AComponent),沒有清理 Observable 並取消訂閱訊號。當使用者開啟 Chrome Dev Console 時,#timer3$ console log 兩個計時器。

範例3:使用DestroyRef.onDestroy回呼清理 Observable

export default class AComponent {
 injector = inject(Injector);
 destroyRef$ = this.injector.get(DestroyRef)

 #stop = new Subject<void>();
 #timer4$ = timer(0, 1000).pipe(
   tap((v) => console.log(`#timer4 ->`, v)),
   takeUntil(this.#stop),
 );
 takeUntilSignal = toSignal(this.#timer4$, { manualCleanup: true })

 constructor() {
   this.destroyRef$.onDestroy(() => this.#stop.next());
 }
}
<p>takeUntil Signal + Subject: {{ takeUntilSignal() }}</p>

#timer4$ 是一個每秒發出一個數字的 Obervable。 當使用者路由到 BComponent 元件時,AComponent 將被銷毀,並且 this.destroyRef$.onDestroy 運行回呼以向 #stop Subject 發出一個數字。 takeUntil(this.#stop) 清理 #timer4$ Observable 並取消訂閱 takeUntilSignal 訊號。

當使用者再次路由到 AComponent 元件時,元件會建立一個新訊號和一個從0 開始的新計時器。
當使用者開啟 Chrome Dev Console時,先前的 #timer4$ 不會記錄 console log,但新的 #timer4$ 會 console log。

#timer4# 沒有 takeUntilOnDestroy 回呼時,會導致 AComponent 銷毀時記憶體洩漏。這是因為 manualCleanup 選項忽略了 creation context(AComponent),沒有清理 Observable並取消訂閱訊號。當使用者開啟 Chrome Dev Console,#timer4$ console log 兩個計時器。

結論:

  • ToSignalOptions有一個 manualCleanup 屬性來控制何時清理 Observable 並取消訂閱訊號。
  • 當使用 manualCleanup 時,toSignal 函數會忽略 injection context。假設元件的 injector 被傳遞給 toSignal 函數,且 manualCleanup 為 true,則當元件被銷毀時,Observable 尚未清理。因此,就會發生記憶體洩漏。
  • toSignal 函數具有 manualCleanup 屬性時,有多種技術可以清理 Observable 以防止記憶體洩漏,例如使用 DestroyRef.OnDestroy 在元件被銷毀時執行回呼函數、takeUntiltakeUntilDestroyed RxJS 運算子。
  • 使用 toSignal 函數的預設行為來清理 Observables 和取消訂閱訊號。 當使用者想要執行自己的清理並且他們知道自己在做什麼時,可以使用 manualCleanup 選項。

參考:


上一篇
Day 43 - toSignal 函數的初始值
系列文
Signal API in Angular44
.
圖片
  直播研討會

尚未有邦友留言

立即登入留言