第 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將其轉換為signal。 memoryLeakTimerSignal立即訂閱Observable並透過root injector's context取消訂閱。當 BComponent在MemoryLeakService service之前被銷毀時,可能會導致memory leaks。
按一下"Load Memory Leak Component"按鈕,然後開啟Chrome開發控制台以觀察log messages。

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

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

基本上,AComponent不會引入任何memory leaks,因為它遵循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);
AComponent注入一個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;
AComponent直接引用luke signal並在範本中顯示該值。
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值。
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運算子。toSignal並傳signal。@Injectable()移除{providedIn: 'root'}選項,並在組件的providers array中provide service。我認為這很很花工夫。鐵人賽的第八天就這樣結束了。
覺得可以順便講一下在route 節點上面provide service 實例時, 實例其實不會因為切換路由而消滅的問題, 還有這個問題對於 toSignal/observable 訂閱造成的影響~
thanks for letting me know. I did not realize memory leak also happens in routing.
謝謝你讓我知道。我沒有意識到memory leaks也發生在路由中。