當 toSignal
函數在 root-level 服務中建立訊號並將其傳回給元件時,creation context 就是 root-level 服務。當元件被銷毀時,root-level 服務,並不清理 Observable
。因此,訊號未被取消訂閱,且應用程式存在記憶體洩漏 (memory leak)。
有使用 toSignal 的建議:
toSignal
函數,以便 creation context 是該元件。當元件被銷毀時,它會清理 Observable 並取消訂閱訊號。toSignal
函數,以便 injector 的 DestroyRef 在元件被銷毀時清理 Observable。toSignal
函數創建的任何訊號。如果我們不想將責任委託給 creation context 或 injector,我們將指定 manualCleanup
選項為 true 並手動取消訂閱訊號。
在大多數情況下,使用 DestroyRef
清理 Observable 並取消訂閱訊號的預設行為就足夠了。 當其他一切都不適用時,請使用 manualCleanup
選項。
interface ToSignalOptions<T> {
manualCleanup?: boolean | undefined;
}
我示範了一個記憶體洩漏的範例,其中指定了 manualCleanup
和 injector
。
此外,我還示範了手動取消訂閱 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)。
@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。
@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。
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 兩個計時器。
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#
沒有 takeUntil
和 OnDestroy
回呼時,會導致 AComponent
銷毀時記憶體洩漏。這是因為 manualCleanup
選項忽略了 creation context(AComponent),沒有清理 Observable並取消訂閱訊號。當使用者開啟 Chrome Dev Console,#timer4$
console log 兩個計時器。
manualCleanup
屬性來控制何時清理 Observable 並取消訂閱訊號。manualCleanup
時,toSignal
函數會忽略 injection context。假設元件的 injector 被傳遞給 toSignal
函數,且 manualCleanup
為 true,則當元件被銷毀時,Observable 尚未清理。因此,就會發生記憶體洩漏。toSignal
函數具有 manualCleanup
屬性時,有多種技術可以清理 Observable 以防止記憶體洩漏,例如使用 DestroyRef.OnDestroy
在元件被銷毀時執行回呼函數、takeUntil
和takeUntilDestroyed
RxJS 運算子。toSignal
函數的預設行為來清理 Observables 和取消訂閱訊號。 當使用者想要執行自己的清理並且他們知道自己在做什麼時,可以使用 manualCleanup
選項。