Применение декораторов в Angular. «RxjsAutoUnsubscribe»

Поскольку Angular поставляется совместно с языком программирования TypeScript, то нам становится доступна возможность применения декораторов. Декоратор - это шаблон проектирования, который позволяет динамически добавлять функциональность к уже существующему коду, не изменяя его логику.

Чем это полезно?

Angular довольно тесно интегрирован с реактивной библиотекой событийного программирования RxJS Observables и системой управления состоянием NgRx. Одна из лучших практик применения Observables в такой связке - отписка от событий.
Если не производить отписку от событий, то в какой-то момент приложение будет использовать чрезмерно много системной памяти, что приведет к ошибкам и зависаниям системы. см. demo

Хорошей практикой решения этой проблемы является отписка от событий при завершении работы компонента (в жизненном цикла приложения - это хук ngOnDestroy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
products$ : Observable
users$: Observable
constructor(private store: Store) {
this.products$ = this.store.select("products").subscribe()
this.user$ = this.store.select("users").subscribe()
}
ngOnDestroy() {
this.products$.unsubscribe()
this.users$.unsubscribe()
}
}

Данный вариант решения создает лишнюю ментальную нагрузку. Добавляя новое Observable поле нужно дополнительно помнить, что если мы подписались на него, то нужно не забыть отписаться, иначе начнутся проблемы. Существует более элегантное решение, которое основано на применении декораторов.

Декоратор представляет собой функцию, которая исполняется перед выполнением целевой функции или конструктора класса. В нашем случае мы будем применять декоратор на класс. Функция - декоратор представлена ниже.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function rxjsAutoUnsubscribe(constructor: any) {
const ngOnDestroy = constructor.prototype.ngOnDestroy;
const subs$: Subscription[] = [];
constructor.prototype.ngOnDestroy = function () {
for (const prop in this) {
const property = this[prop];
if (typeof property.unsubscribe === "function" && !subs$.includes(property))
subs$.push(property);
}
for (const ob$ of subs$) {
ob$.unsubscribe();
}
ngOnDestroy?.apply();
};
}

Таким образом, если метод rxjsAutoUnsubscribe найдет у класса поля, у которых есть метод unsubscribe, то при завершении работы приложения (выполнении хука ngOnDestroy) произойдет автоматическая отписка от событий.

Пример целиком:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
@rxjsAutoUnsubscribe
export class AppComponent implements OnDestroy {
products$ : Observable
users$: Observable
constructor(private store: Store) {
this.products$ = this.store.select("products").subscribe()
this.users$ = this.store.select("users").subscribe()
}
}

export function rxjsAutoUnsubscribe(constructor: any) {
const ngOnDestroy = constructor.prototype.ngOnDestroy;
const subs$: Subscription[] = [];
constructor.prototype.ngOnDestroy = function () {
for (const prop in this) {
const property = this[prop];
if (typeof property.unsubscribe === "function" && !subs$.includes(property))
subs$.push(property);
}
for (const ob$ of subs$) {
ob$.unsubscribe();
}
ngOnDestroy?.apply();
};
}