iT邦幫忙

2024 iThome 鐵人賽

DAY 8
0
JavaScript

Signal API in Angular系列 第 8

Day 08 - 避免root-level service和 toSignal中的memory leak

  • 分享至 

  • xImage
  •  

第 5 天,我介紹了將Observable轉換為signal的toSignal函數。在我的範例中,我在組件中使用 toSignal`來避免root-level service中可能意外發生的memory leaks。

Root-level service有@Injectable(provideIn: ‘root’)的decorator。

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class MemoryLeakService {}

MemoryLeakService在root injectors中存在,並且僅在應用程式終止時才會終止。當root-level service使用toSignal和不會完成的Observable時,這可能會導致memory leaks。

讓我們建立範例程式碼

import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { MemoryLeakService } from './memory-leak.service';

@Component({
 selector: 'app-b',
 standalone: true,
 template: `
   <p>B Component</p>
   <p>Memory leak Timer Signal: {{ timerSignal() }}</p>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BComponent {
 timerSignal = inject(MemoryLeakService).memoryLeakTimerSignal;
}
import { Route } from '@angular/router';

export const routes: Route[] = [
 {
   path: 'a',
   loadComponent: () => import('./a.component').then((m) => m.AComponent)
 },
 {
   path: 'b',
   loadComponent: () => import('./b.component').then((m) => m.BComponent)
 },
]
import { Component, inject } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [RouterOutlet],
 template: `
   <h1>Hello from {{ name }}!</h1>
   <h2>{{ description }}</h2>
   <div>
     <button (click)="router.navigateByUrl('/a')">
       Load Component A
     </button>
     <button (click)="router.navigateByUrl('/b')">
       Load Memory Leak Component
     </button>
   </div>
   <router-outlet />
 `,
})
export class App {
 name = 'IT Home Ironman 2024 day 8';
 description = 'toSignal memory leaks';
 router = inject(Router);
}

此示範有一些路由到AComponent(/a)和BComponent(/b)。 BComponent注入MemoryLeakService service,可能導致memory leaks。

import { Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { timer, tap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class MemoryLeakService {
 memoryLeakTimerSignal = toSignal(
   timer(0, 1000).pipe(tap((v) => console.log(`memoryLeakTimerSignal ->`, v)))
 );
}

timer運算子每秒發出一個數字,然後呼叫toSignal將其轉換為signalmemoryLeakTimerSignal立即訂閱Observable並透過root injector's context取消訂閱。當 BComponentMemoryLeakService service之前被銷毀時,可能會導致memory leaks。

按一下"Load Memory Leak Component"按鈕,然後開啟Chrome開發控制台以觀察log messages。

https://ithelp.ithome.com.tw/upload/images/20240817/20168314qQuTGB24ej.png

按一下"Load Component A"按鈕路由到/a並顯示AComponent。 即使BComponent被銷毀,log messages也會繼續輸出。

https://ithelp.ithome.com.tw/upload/images/20240817/20168314Z0XkA3u6og.png

當再次按一下"Load Memory Leak Component"時,AComponent中的signals完成,而memoryLeakTimerSignal signal繼續發出值。

https://ithelp.ithome.com.tw/upload/images/20240817/20168314zKUo4bOEMf.png

基本上,AComponent不會引入任何memory leaks,因為它遵循toSignal的良好規則。

Good rules of toSignal

import { Component, ChangeDetectionStrategy, inject, Injector } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { NoMemoryLeakService } from './no-memory-leak.service';
import { NonRootNoMemoryLeakService } from './non-root-no-memory-leak.service';

@Component({
 selector: 'app-a',
 standalone: true,
 template: `
   <p>A Component</p>
   <p>toSignal within component: {{ runningNumbers() }}</p>
   <p>pass a component injector to service: {{ injectorRunningNumbers() }}</p>
   @if (luke(); as luke) {
     <p>Luke: {{ luke.name }}</p>
   }
   <p>Non Root Timer Signal: {{ nonRootTimerSignal() }}</p>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
 providers: [
   {
     provide: NonRootNoMemoryLeakService,
     useClass: NonRootNoMemoryLeakService,
   }
 ]
})
export class AComponent {
  injector = inject(Injector);
 noLeakService = inject(NoMemoryLeakService);

 // use toSignal in the component
 runningNumbers = toSignal(this.noLeakService.timer$);
 // use component's injector to create a signal
 injectorRunningNumbers = this.noLeakService.createTimer(this.injector);

 // http request always complete the Observable
 luke = this.noLeakService.luke;

 // provide the service within the component
 nonRootTimerSignal = inject(NonRootNoMemoryLeakService).nonRootTimerSignal;
}
  • toSignal使用組件的injector,以便在組件被銷毀時取消訂閱signal。
import { HttpClient } from '@angular/common/http';
import { inject, Injectable, Injector } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { timer, tap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class NoMemoryLeakService {
 timer$ = timer(0, 1000).pipe(tap((v) => console.log('timer$ ->', v)));

 // http request always completes the Observable; therefore, it is alright to use toSignal in a root-level service
 http = inject(HttpClient);
 luke = toSignal(this.http.get<{ name: string }>('https://swapi.dev/api/people/1')
   .pipe(tap(() => console.log('luke is retrieved'))));

 // use component's injector to create the signal
 createTimer(injector: Injector) {
   return toSignal(
     timer(0, 1500).pipe(tap((v) => console.log(`createTimer ->`, v))),
     { injector }
   )
 }
}

NoMemoryLeakService定義了一個createTimer method,該method接受injector並將其傳遞給 toSignal以建立signal

injector = inject(Injector);
injectorRunningNumbers = this.noLeakService.createTimer(this.injector);

BComponent注入一個injector並將其傳遞給service以建立signal。

  • Observable一定可以完成時,可以在root-level service中使用toSignal
luke = toSignal(this.http.get<{ name: string }>('https://swapi.dev/api/people/1')
   .pipe(tap(() => console.log('luke is retrieved'))));

HTTP request總是完成Observable;因此,在root-level servide中使用toSignal是可以的。

// http request always complete the Observable
luke = this.noLeakService.luke;

BComponent直接引用luke signal並在範本中顯示該值。

  • 在組件中提供service,使兩者同時銷毀。
import { Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { timer, tap } from 'rxjs';

@Injectable()
export class NonRootNoMemoryLeakService {
 nonRootTimerSignal = toSignal(
   timer(0, 2000).pipe(tap((v) => console.log(`Non Root TimerSignal ->`, v)))
 );
}

NonRootNoMemoryLeakService沒有{providedIn: ‘root’}選項;因此,它不在root injector中。 AComponent必須提供service才能被注入和存取。

@Component({
 selector: 'app-a',
 standalone: true,
 template: `
   <p>A Component</p>
   <p>Non Root Timer Signal: {{ nonRootTimerSignal() }}</p>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
 providers: [
   {
     provide: NonRootNoMemoryLeakService,
     useClass: NonRootNoMemoryLeakService,
   }
 ]
})
export class AComponent {
  // provide the service within the component
  nonRootTimerSignal = inject(NonRootNoMemoryLeakService).nonRootTimerSignal;
}

nonRootTimerSignal注入NonRootNoMemoryLeakService並存取nonRootTimerSignal signal。 然後,範本中顯示signal值。

  • 在組件中使用toSignal
import { HttpClient } from '@angular/common/http';
import { inject, Injectable, Injector } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { timer, tap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class NoMemoryLeakService {
 timer$ = timer(0, 1000).pipe(tap((v) => console.log('timer$ ->', v)));
}

Root-level service可以安全地建立Observables,因為它們在呼叫之前是延遲載入的。

@Component({
 selector: 'app-a',
 standalone: true,
 template: `
   <p>A Component</p>
   <p>toSignal within a component: {{ runningNumbers() }}</p>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AComponent {
 noLeakService = inject(NoMemoryLeakService);

 // use toSignal in the component
 runningNumbers = toSignal(this.noLeakService.timer$);
}

AComponent中,toSignal訂閱了this.noLeakService.timer$ Observable並在範本中顯示signal值。

結論

  • Observable不會完成時,請勿在root-level service中使用toSignal,例如RxJS interval/interval運算子。
  • 當您知道Observable一定可以完成時,可以在root-level service中使用toSignal,例如,HTTP request或需要X個數字並完成的RxJS interval/timer運算子。
  • 組件建立一個injector並將其傳遞給rool-level service的method。然後,此method可以將injector作為選項提供給toSignal並傳signal。
  • @Injectable()移除{providedIn: 'root'}選項,並在組件的providers array中provide service。我認為這很很花工夫。
  • 我建議在組件中使用toSignal,以避免不必要的記憶體洩漏。

鐵人賽的第八天就這樣結束了。

參考:


上一篇
Day 07 - ngXtension中的 toObservableSignal - 天使與魔鬼的結合體
下一篇
Day 09 - Template-driven form和Signal執行 雙向NgMode綁定
系列文
Signal API in Angular39
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Mizok
iT邦新手 3 級 ‧ 2024-08-17 13:17:02

覺得可以順便講一下在route 節點上面provide service 實例時, 實例其實不會因為切換路由而消滅的問題, 還有這個問題對於 toSignal/observable 訂閱造成的影響~

Mizok iT邦新手 3 級 ‧ 2024-08-17 13:18:19 檢舉

thanks for letting me know. I did not realize memory leak also happens in routing.

謝謝你讓我知道。我沒有意識到memory leaks也發生在路由中。

我要留言

立即登入留言