第 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);
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並在範本中顯示該值。
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也發生在路由中。