6 способов отписаться от Observables в Angular

от автора

Обратная сторона подписки на Observable

У Observables есть метод subscribe, который вызывается с помощью callback-функции, чтобы получить значения, отправляемые (emit) в Observable. В Angular он используется в компонентах/директивах, а особенно в router-модуле, NgRx и HTTP.
Если мы подпишемся на поток, он останется открытым и будет вызываться всякий раз, когда в него передаются значения из любой части приложения, до тех пор, пока он не будет закрыт с помощью вызова unsubscribe.

@Component({...}) export class AppComponent implements OnInit {     subscription: Subscription     ngOnInit () {         const observable = Rx.Observable.interval(1000);         this.subscription = observable.subscribe(x => console.log(x));     } } 

В данной реализации мы используем интервал для отправки значений каждую секунду. Мы подписываемся на него, чтобы получить отправленное значение, а наша callback-функция пишет результат в консоль браузера.
Теперь, если AppComponent будет уничтожен, например, после выхода из компонента или с помощью метода destroy(), мы все равно увидим лог консоли в браузере. Это связано с тем, что хотя AppComponent был уничтожен, подписка не была отменена.
Если подписка не закрыта, callback-функция будет непрерывно вызываться, что приведет к серьёзной утечке памяти и проблемам с производительностью. Для того, чтобы избежать утечек необходимо каждый раз «отписываться» от Observable.

1. Использование метода unsubscribe

Любой Subscription имеет функцию unsubscribe() для освобождения ресурсов и отмены исполнения Observable. Чтобы предотвратить утечку памяти необходимо отменить подписки с помощью метода unsubscribe в Observable.
В Angular нужно отписаться от Observable, когда компонент уничтожается. К счастью, в Angular есть хук ngOnDestroy, который вызывается перед уничтожением компонента, что позволяет разработчикам обеспечить очистку памяти, избежать зависания подписок, открытых портов и прочих «выстрелов в ногу».

@Component({…}) export class AppComponent implements OnInit, OnDestroy {     subscription: Subscription      ngOnInit () {         const observable = Rx.Observable.interval(1000);         this.subscription = observable.subscribe(x => console.log(x));     }     ngOnDestroy() {         this.subscription.unsubscribe()     } }

Мы добавили ngOnDestroy в наш AppComponent и вызвали метод unsubscribe на Observable this.subscription. Когда AppComponent будет уничтожен (с помощью перехода по ссылке, метода destroy() и т. д.), подписка не будет зависать, интервал будет остановлен, а в браузере больше не будет логов консоли.

А что если у нас есть несколько подписок?

@Component({…}) export class AppComponent implements OnInit, OnDestroy {     subscription1$: Subscription;     subscription2$: Subscription;          ngOnInit () {         const observable1$ = Rx.Observable.interval(1000);         const observable2$ = Rx.Observable.interval(400);         this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));         this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x));     }         ngOnDestroy() {         this.subscription1$.unsubscribe();         this.subscription2$.unsubscribe();     } }

В AppComponent две подписки и обе отписались в хуке ngOnDestroy, предотвращая утечку памяти.
Так же можно собрать все подписки в массив и отписаться от них в цикле:

@Component({…}) export class AppComponent implements OnInit, OnDestroy {     subscription1$: Subscription;      subscription2$: Subscription;      subscriptions: Subscription[] = [];     ngOnInit () {         const observable1$ = Rx.Observable.interval(1000);         const observable2$ = Rx.Observable.interval(400);         this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));         this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x));         this.subscriptions.push(this.subscription1$);         this.subscriptions.push(this.subscription2$);     }         ngOnDestroy() {         this.subscriptions.forEach((subscription) => subscription.unsubscribe());     } }

Метод subscribe возвращает объект RxJS типа Subscription. Он представляет собой одноразовый ресурс. Подписки могут быть сгруппированы с помощью метода add, который прикрепит дочернюю подписку к текущей. Когда подписка отменяется, все ее дочерние элементы также отписываются. Попробуем переписать наш AppComponent:

@Component({…}) export class AppComponent implements OnInit, OnDestroy {     subscription: Subscription;         ngOnInit () {         const observable1$ = Rx.Observable.interval(1000);         const observable2$ = Rx.Observable.interval(400);         const subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));         const subscription2$ = observable.subscribe(x => console.log("From interval 400" x));         this.subscription.add(subscription1$);         this.subscription.add(subscription2$);     }         ngOnDestroy() {         this.subscription.unsubscribe()     } }

Так мы отпишем this.subscripton1$ и this.subscripton2$ в момент уничтожения компонента.

2. Использование Async | Pipe

Pipe async подписывается на Observable или Promise и возвращает последнее переданное значение. Когда новое значение отправляется, pipe async чекает данный компонент на отслеживание изменений. Если компонент уничтожается, pipe async автоматически отписывается.

@Component({     ...,     template: `         <div>         Interval: {{observable$ | async}}         </div>     ` }) export class AppComponent implements OnInit {     observable$;     ngOnInit () {         this.observable$ = Rx.Observable.interval(1000);     } } 

При инициализации AppComponent создаст Observable из метода интервала. В шаблоне observable$ передается async. Он подпишется на observable$ и отобразит его значение в DOM. Так же он отменит подписку, когда AppComponent будет уничтожен. Pipe async в своем классе содержит хук ngOnDestroy, поэтому тот вызовется, когда его view будет уничтожен.
Pipe async очень удобно использовать, потому что он сам будет подписываться на Observable и отписываться от них. И мы можем теперь не беспокоиться если забудем отписаться в ngOnDestroy.

3. Использование операторов RxJS take*

RxJS содержит полезные операторы, которые можно использовать декларативным способом, чтобы отменять подписки в нашем Angular-проекте. Один из них — операторы семейства *take**:

  • take(n)
  • takeUntil(notifier)
  • takeWhile(predicate)

take(n)
Этот оператор emit-ит исходную подписку указанное количество раз и завершается. Чаще всего в take передается единица (1) для подписки и выхода.
Данный оператор полезно использовать, если мы хотим, чтобы Observable передал значение один раз, а затем отписался от потока:

@Component({...}) export class AppComponent implements OnInit {     subscription$;     ngOnInit () {         const observable$ = Rx.Observable.interval(1000);         this.subscription$ = observable$.pipe(take(1)).         subscribe(x => console.log(x));     } } 

subscription$ отменит подписку, когда интервал передаст первое значение.
Обратите внимание: даже если AppComponent будет уничтожен, subscription$ не отменит подписку, пока интервал не передаст значение. Поэтому все равно лучше убедиться, что все отписано в хуке ngOnDestroy:

@Component({…}) export class AppComponent implements OnInit, OnDestroy {     subscription$;     ngOnInit () {         var observable$ = Rx.Observable.interval(1000);         this.subscription$ = observable$.pipe(take(1)).subscribe(x => console.log(x));     }     ngOnDestroy() {         this.subscription$.unsubscribe();     } }

takeUntil(notifier)
Этот оператор emit-ит значения из исходного Observable, до тех пор, пока notifier не отправит сообщение о завершении.

@Component({…}) export class AppComponent implements OnInit, OnDestroy {     notifier = new Subject();         ngOnInit () {         const observable$ = Rx.Observable.interval(1000);         observable$.pipe(takeUntil(this.notifier)).subscribe(x => console.log(x));     }         ngOnDestroy() {         this.notifier.next();         this.notifier.complete();     } }

У нас есть дополнительный Subject для уведомлений, который отправит команду, чтобы отписать this.subscription. Мы pipe-им Observable в takeUntil до тех пор, пока мы подписаны. TakeUntil будет emit-ить сообщения интервала, пока notifier не отменит подписку observable$. Удобнее всего помещать notifier в хук ngOnDestroy.

takeWhile(predicate)
Этот оператор будет emit-ить значения Observable, пока они соответствуют условию предиката.

@Component({...}) export class AppComponent implements OnInit {     ngOnInit () {         const observable$ = Rx.Observable.interval(1000);         observable$.pipe(takeWhile(value => value < 10)).subscribe(x => console.log(x));     } } 

Мы pip-им observable$ с оператором takeWhile, который будет отправлять значения до тех пор, пока они меньше 10. Если придет значение большее или равное 10, оператор отменит подписку. Важно понимать, что подписка observable$ будет открыта, пока интервал не выдаст 10. Поэтому для безопасности мы добавляем хук ngOnDestroy, чтобы отписаться от observable$, когда компонент уничтожен.

@Component({…}) export class AppComponent implements OnInit, OnDestroy {     subscription$;     ngOnInit () {         var observable$ = Rx.Observable.interval(1000);         this.subscription$ = observable$.pipe(takeWhile(value => value < 10)).subscribe(x => console.log(x));     }         ngOnDestroy() {          this.subscription$.unsubscribe();    } }

4. Использование оператора RxJS first

Этот оператор похож на объединенный take(1) и takeWhile.
Если он вызывается без параметра, то emit-ит первое значение Observable и завершается. Если он вызывается с функцией предиката, то emit-ит первое значение исходного Observable, которое соответствует условию функции предиката, и завершается.

@Component({...}) export class AppComponent implements OnInit {     observable$;     ngOnInit () {         this.observable = Rx.Observable.interval(1000);         this.observable$.pipe(first()).subscribe(x => console.log(x));     } } 

observable$ завершится, если интервал передаст свое первое значение. Это означает, что в консоли мы увидим только 1 сообщение лога.

@Component({...}) export class AppComponent implements OnInit {     observable$;     ngOnInit () {         this.observable$ = Rx.Observable.interval(1000);        this.observable$.pipe(first(val => val === 10)).subscribe(x => console.log(x));     } } 

Здесь first не будет emit-ить значения, пока интервал не передаст 10-ку, а затем завершит observable$. В консоли увидим только одно сообщение.
В первом примере, если AppComponent уничтожен до того, как first получит значение из observable$, подписка будет по-прежнему открыта до получения первого сообщения.
Так же, во втором примере, если AppComponent уничтожен до того, как интервал отдаст подходящее под условие оператора значение, подписка будет по-прежнему открыта до тех пор, пока интервал не отдаст 10. Поэтому, чтобы обеспечить безопасность, мы должны явно отменять подписки в хуке ngOnDestroy.

5. Использование Декоратора для автоматизации отписки

Все мы люди, нам свойственно забывать. Большинство предыдущих способов опираются на хук ngOnDestroy, чтобы удостовериться в очищении подписки перед уничтожением компонента. Но мы можем забыть прописать их в ngOnDestroy, — может быть из-за дедлайна, или нервного клиента, который знает, где вы живете…
В этом случае мы можем использовать Декораторы в наших Angular-проектах, чтобы автоматически отписаться от всех подписок в компоненте.
Вот пример такой полезной реализации:

function AutoUnsub() {     return function(constructor) {         const orig = constructor.prototype.ngOnDestroy;         constructor.prototype.ngOnDestroy = function() {             for(let prop in this) {                 const property = this[prop];                 if(typeof property.subscribe === "function") {                     property.unsubscribe();                 }            }            orig.apply();         }     } } 

Этот AutoUnsub является декоратором, который можно применять к классам в нашем Angular-проекте. Как видите, он сохраняет оригинальный хук ngOnDestroy, затем создает новый и подключает его к классу, к которому тот применяется. Таким образом, когда класс уничтожается, вызывается новый хук. Его функция просматривает свойства класса, и если находит Observable, то отписывается от него. Затем он вызывает оригинальный хук ngOnDestroy в классе, если тот имеется.

@Component({...}) @AutoUnsub export class AppComponent implements OnInit {     observable$;     ngOnInit () {         this.observable$ = Rx.Observable.interval(1000);         this.observable$.subscribe(x => console.log(x))     } }

Мы применяем его к нашему AppComponent и больше не беспокоимся о том, что забыли отписаться от observable$ в ngOnDestroy, — декоратор сделает это за нас.
Но у этого способа есть и обратная сторона — возникнут ошибки если в нашем компоненте будет Observable без подписки.

6. Использование tslint

Иногда может быть полезным сообщение от tslint, чтобы сообщить в консоли, что у наших компонентов или директив не объявлен метод ngOnDestroy. Можно добавить пользовательское правило в tslint, чтобы предупреждать в консоли в момент выполнения lint или build, что в наших компонентах нет хука ngOnDestroy:

// ngOnDestroyRule.tsimport * as Lint from "tslint" import * as ts from "typescript"; import * as tsutils from "tsutils"; export class Rule extends Lint.Rules.AbstractRule {     public static metadata: Lint.IRuleMetadata = {         ruleName: "ng-on-destroy",         description: "Enforces ngOnDestory hook on component/directive/pipe classes",         optionsDescription: "Not configurable.",         options: null,         type: "style",         typescriptOnly: false     }         public static FAILURE_STRING = "Class name must have the ngOnDestroy hook";         public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {         return this.applyWithWalker(new NgOnDestroyWalker(sourceFile, Rule.metadata.ruleName, void this.getOptions()))     } }  class NgOnDestroyWalker extends Lint.AbstractWalker {     visitClassDeclaration(node: ts.ClassDeclaration) {         this.validateMethods(node);     }         validateMethods(node: ts.ClassDeclaration) {         const methodNames = node.members.filter(ts.isMethodDeclaration).map(m => m.name!.getText());         const ngOnDestroyArr = methodNames.filter( methodName => methodName === "ngOnDestroy");         if( ngOnDestroyArr.length === 0)             this.addFailureAtNode(node.name, Rule.FAILURE_STRING);     } }

Если у нас есть такой компонент без ngOnDestroy:

@Component({...}) export class AppComponent implements OnInit {     observable$;     ngOnInit () {         this.observable$ = Rx.Observable.interval(1000);     this.observable$.subscribe(x => console.log(x));     } } 

Lint-инг AppComponent-а предупредит нас о пропущенном хуке ngOnDestroy:

$ ng lint Error at app.component.ts 12: Class name must have the ngOnDestroy hook 

Заключение

Повисшая или открытая подписка могут привести к утечкам памяти, ошибкам, нежелательному поведению или снижению производительности приложений. Чтобы этого избежать, мы рассмотрели разные способы отписки от Observable в проектах Angular. А какой использовать в конкретной ситуации — решать вам.

ссылка на оригинал статьи https://habr.com/ru/post/484762/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *