Unsubscribe một Subscription trong RxJS
- 06-01-2024
- Toanngo92
- 0 Comments
Mục lục
Angular EventEmitter
Angular EventEmitter là một công cụ mạnh mẽ trong Angular để tạo và quản lý các sự kiện (events). Nó thực chất là một dạng của Subject trong RxJS. Bạn có thể sử dụng EventEmitter để giao tiếp giữa các component, có thể query các instance của component và đăng ký (subscribe) vào các sự kiện này.
Khi bạn sử dụng event binding trong Angular, không cần phải lo lắng về việc unsubscribe vì Angular sẽ tự động làm điều này cho bạn. Tuy nhiên, khi bạn tự thực hiện subscribe vào một EventEmitter, bạn cần nhớ phải thực hiện việc unsubscribe để tránh nguy cơ xảy ra memory leak. Điều này có thể được làm bằng cách sử dụng cơ chế unsubscribe như ngOnDestroy hoặc các phương thức unsubscribe cụ thể.
Subject Trong một Component
Trong một component, bạn có thể sử dụng một instance của Subject để tạo ra và quản lý các sự kiện. Xem xét hai trường hợp khác nhau khi sử dụng Subject.
Trường hợp đầu tiên, khi bạn chỉ đơn giản subscribe trực tiếp vào Subject mà không áp dụng các pipe operators khác, bạn không cần phải lo lắng về việc unsubscribe khi component bị hủy bỏ. Khi component bị hủy bỏ, instance của Subject cũng sẽ bị loại bỏ khỏi bộ nhớ.
Tuy nhiên, trong trường hợp thứ hai, khi bạn áp dụng các operators như mergeMap
và interval
, bạn tạo ra một stream mới từ Subject gốc. Trong trường hợp này, có thể stream này sẽ không bao giờ dừng lại ngay cả khi component đã bị hủy bỏ. Điều này gây ra một memory leak, nơi stream vẫn tiếp tục hoạt động sau khi component đã bị hủy bỏ. Để tránh memory leak, bạn cần phải unsubscribe từ stream này khi component bị hủy bỏ.
import { Component, OnInit, OnDestroy } from "@angular/core";
import { interval, Subject, Subscription } from "rxjs";
import { mergeMap, scan } from "rxjs/operators";
@Component({
selector: "app-product",
templateUrl: "./product.component.html",
styleUrls: ["./product.component.scss"],
})
export class ProductComponent implements OnInit, OnDestroy {
grandTotal$ = new Subject<number>();
subscription = Subscription.EMPTY; // Lưu lại subscription
constructor() {}
ngOnInit(): void {
this.subscription = this.grandTotal$
.pipe(
mergeMap((total) =>
interval(1000).pipe(scan((acc, value) => acc + value, total))
)
)
.subscribe({
next: (grandTotal) => {
console.log(grandTotal);
},
});
}
ngOnDestroy(): void {
this.subscription.unsubscribe(); // Unsubscribe khi component bị hủy bỏ
}
}
Khi sử dụng subscription như trong ví dụ trên, bạn sẽ đảm bảo rằng stream sẽ bị dừng khi component không còn tồn tại, giúp tránh memory leak trong ứng dụng của bạn.
ActivatedRoute trong RxJS
ActivatedRoute
trong Angular là một dạng Observable có chứa thông tin về route hiện tại. Khi bạn inject service này vào một component, mỗi khi component được tạo ra, một instance của ActivatedRoute
cũng được tạo ra. Nó chứa các Observable như paramMap
, queryParamMap
để bạn có thể theo dõi sự thay đổi của các tham số trong URL.
Nếu bạn chỉ subscribe vào các Observable như paramMap
hoặc queryParamMap
mà không có các xử lý phức tạp hơn, thì bạn có thể không cần phải thực hiện việc unsubscribe. Khi component bị hủy, Angular sẽ tự động loại bỏ instance của ActivatedRoute
này khỏi bộ nhớ cùng với instance của component.
Tuy nhiên, khi bạn sử dụng các thông tin từ param hoặc query để thực hiện các tác vụ như gửi AJAX request, lắng nghe WebSocket, bạn sẽ tạo ra một stream phức tạp hơn từ các Observable như queryParamMap
. Trong trường hợp này, tương tự như khi sử dụng Subject
trong ví dụ trước, bạn cũng cần thực hiện việc unsubscribe để tránh memory leak.
import { Component, OnInit, OnDestroy } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { interval, Subscription } from "rxjs";
import { mergeMap } from "rxjs/operators";
@Component({
selector: "app-product",
templateUrl: "./product.component.html",
styleUrls: ["./product.component.scss"],
})
export class ProductComponent implements OnInit, OnDestroy {
subscription = Subscription.EMPTY;
constructor(private activatedRouter: ActivatedRoute) {}
ngOnInit(): void {
this.subscription = this.activatedRouter.queryParamMap
.pipe(
mergeMap((query) => {
// Xử lý thông tin từ query
console.log(query);
return interval(1000);
})
)
.subscribe({
next: (data) => {
console.log(data);
},
});
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
Khi bạn sử dụng ActivatedRoute
để theo dõi các thay đổi trong URL và thực hiện các tác vụ phức tạp, việc unsubscribe là cần thiết để đảm bảo rằng các stream được tạo ra từ các Observable này sẽ bị dừng và tránh gây ra memory leak khi component bị hủy.
Subscribe Vào Một Observable Từ Một Service
Trong các ví dụ trước, bạn có thể thấy các Observable thường có vòng đời tương đương với vòng đời của component chứa chúng. Tuy nhiên, khi bạn sử dụng một service có đặc tính singleton (tồn tại một instance trong suốt vòng đời của ứng dụng), việc subscribe vào một Observable từ service đó mà không unsubscribe có thể gây ra vấn đề gì?
Ví dụ, bạn có một service CartService và bạn subscribe vào cart$ từ service này mà quên unsubscribe.
@Injectable({
providedIn: "root",
})
export class CartService {
private _cart$ = new BehaviorSubject<CartItem[]>([]);
cart$ = this._cart$.asObservable();
constructor() {}
addToCart(product: Product): void {
// Logic thêm sản phẩm vào giỏ hàng
}
}
class ProductCartSubscriber extends Subscriber<CartItem[]> {
next(cartItems: CartItem[]): void {
console.log(cartItems);
}
}
export class ProductComponent implements OnInit, OnDestroy {
constructor(private cartService: CartService) {}
ngOnInit(): void {
this.cartService.cart$.subscribe(new ProductCartSubscriber());
}
ngOnDestroy(): void {
// ...
}
}
Trong trường hợp này, ngay cả khi component bị hủy, khi cart$ emit một giá trị mới, next callback vẫn được gọi. Khi bạn so sánh memory, bạn sẽ thấy instance của ProductCartSubscriber vẫn tồn tại trong bộ nhớ.
Đối với các service có vòng đời không tương tự như component hiện tại, việc unsubscribe khi component bị hủy là cần thiết để tránh vấn đề về memory.
export class ProductComponent implements OnInit, OnDestroy {
subscription = Subscription.EMPTY;
constructor(private cartService: CartService) {}
ngOnInit(): void {
this.subscription = this.cartService.cart$.subscribe(
new ProductCartSubscriber()
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
Khi bạn unsubscribe trong ngOnDestroy, instance của ProductCartSubscriber sẽ không còn tồn tại trong bộ nhớ sau khi component bị hủy. Điều này giúp tránh tình trạng memory leak trong ứng dụng của bạn.
HttpClient Có Cần Unsubscribe
HttpClient trong Angular là một loại Observable đặc biệt. Nó chỉ phát ra (emit) một giá trị duy nhất và sau đó kết thúc (complete). Khi một Observable hoàn tất (complete), nó sẽ không phát ra bất kỳ giá trị nào nữa.
Nếu bạn chỉ đơn giản subscribe vào Observable của HttpClient mà không áp dụng các pipe operators phức tạp, và bạn chắc chắn rằng nó không tiếp tục phát ra các giá trị sau khi hoàn tất, thì việc unsubscribe có thể không cần thiết.
Tuy nhiên, nếu bạn áp dụng các operators phức tạp hoặc bạn không chắc chắn liệu Observable đó có kết thúc hay không, thì việc unsubscribe vẫn được khuyến nghị. Điều này giúp đảm bảo rằng không có nguồn dữ liệu nào tiếp tục phát ra sau khi bạn không cần nữa, tránh tình trạng tiêu tốn tài nguyên không cần thiết.
Avoid Unsubscribe
Khi làm việc với Angular, việc nhớ khi nào cần unsubscribe là quan trọng để tránh memory leak. Tuy nhiên, có một số cách tiện lợi mà không cần phải unsubscribe, như sử dụng async pipe cho một Observable hoặc Promise.
Trong Angular, async pipe tự động quản lý việc subscribe và unsubscribe cho bạn. Bạn có thể sử dụng async pipe trong template để làm việc với Observable một cách an toàn:
<div *ngIf="stream$ | async as stream">
<!-- Ví dụ: truy cập vào stream.body -->
{{stream.body}}
</div>
Tuy nhiên, async pipe chỉ nên sử dụng ở ngoài template. Nếu bạn cần sử dụng một Observable nhiều lần, nó sẽ tạo nhiều subscription.
Một kỹ thuật khác là kết hợp async pipe với operator takeUntil ở cuối stream để đảm bảo việc unsubscribe:
export class ProductComponent implements OnInit, OnDestroy {
destroyed$ = new Subject<void>();
constructor(private cartService: CartService) {}
ngOnInit(): void {
this.cartService.cart$
.pipe(takeUntil(this.destroyed$))
.subscribe(new ProductCartSubscriber());
}
ngOnDestroy(): void {
this.destroyed$.next();
this.destroyed$.complete();
}
}
Khi OnDestroy được gọi, chúng ta gửi một tín hiệu (signal) thông qua Subject destroyed$. Stream sử dụng takeUntil sẽ nhận tín hiệu này và dừng lại.
Để tránh việc lặp lại việc tạo Subject destroyed$, bạn có thể tạo một service có life cycle giống như component để quản lý điều này. Ví dụ:
@Injectable() // Không phải root scope
export class DestroyService implements OnDestroy {
public destroyed$ = new Subject<void>();
ngOnDestroy(): void {
this.destroyed$.next();
this.destroyed$.complete();
}
}
@Component({
selector: "app-product",
templateUrl: "./product.component.html",
styleUrls: ["./product.component.scss"],
providers: [DestroyService], // Cung cấp ở mức component
})
export class ProductComponent implements OnInit, OnDestroy {
constructor(
private cartService: CartService,
private destroy: DestroyService
) {}
ngOnInit(): void {
this.cartService.cart$
.pipe(takeUntil(this.destroy.destroyed$))
.subscribe(new ProductCartSubscriber());
}
}
Khi sử dụng service DestroyService, bạn không cần phải lặp lại việc tạo Subject destroyed$ mỗi khi cần unsubscribe, giúp quản lý tốt hơn và tránh được memory leak trong ứng dụng của bạn.