angular - takeuntil - ts subscribe




Angular/RxJs Когда я должен отписаться от подписки (12)

--- Редактировать 4 - Дополнительные ресурсы (2018/09/01)

В недавнем эпизоде ​​« Приключения в Angular» Бен Леш и Уорд Белл обсуждают вопросы о том, как и когда отписаться в компоненте. Обсуждение начинается примерно в 1:05:30.

Уорд упоминает, что right now there's an awful takeUntil dance that takes a lot of machinery а Шай Резник упоминает, что Angular handles some of the subscriptions like http and routing .

В ответ Бен упоминает, что сейчас идут обсуждения, позволяющие Observables подключаться к событиям жизненного цикла компонента Angular, а Ward предлагает Observable событий жизненного цикла, на которые компонент может подписаться, чтобы узнать, когда завершать Observables, поддерживаемые как внутреннее состояние компонента.

Тем не менее, сейчас нам в основном нужны решения, так что вот некоторые другие ресурсы.

  1. Рекомендация по takeUntil() от члена основной команды RxJ Николаса Джеймисона и правило tslint для его принудительного применения. https://blog.angularindepth.com/rxjs-avoiding-takeuntil-leaks-fb5182d047ef

  2. Облегченный пакет npm, который предоставляет оператор Observable, который принимает экземпляр компонента ( this ) в качестве параметра и автоматически отписывается во время ngOnDestroy . https://github.com/NetanelBasal/ngx-take-until-destroy

  3. Еще один вариант вышеупомянутого с немного лучшей эргономикой, если вы не делаете сборки AOT (но мы все должны делать AOT сейчас). https://github.com/smnbbrv/ngx-rx-collector

  4. Пользовательская директива *ngSubscribe которая работает как асинхронный канал, но создает встроенное представление в вашем шаблоне, чтобы вы могли ссылаться на значение «развернутый» во всем шаблоне. https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

В комментарии к блогу Николаса я упоминаю, что чрезмерное использование takeUntil() может быть признаком того, что ваш компонент пытается сделать слишком много, и что следует рассмотреть возможность разделения существующих компонентов на компоненты Feature и Presentational . Вы можете тогда | async | async Observable из компонента Feature во Input компонента Presentational, что означает, что подписки нигде не нужны. Подробнее об этом подходе читайте here

--- Редактировать 3 - «Официальное» решение (2017/04/09)

Я говорил с Уордом Беллом об этом вопросе в NGConf (я даже показал ему этот ответ, который, по его словам, был правильным), но он сказал мне, что команда разработчиков документации для Angular нашла решение этого вопроса, которое не опубликовано (хотя они работают над его утверждением). ). Он также сказал мне, что я могу обновить свой SO-ответ следующей официальной рекомендацией.

Решение, которое мы все должны использовать в будущем, состоит в добавлении private ngUnsubscribe = new Subject(); поле для всех компонентов, которые имеют .subscribe() для Observable s в своем коде класса.

Затем мы вызываем this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); в наших ngOnDestroy() .

Секретный соус (как уже отмечалось @metamaker ) состоит в том, чтобы вызывать takeUntil(this.ngUnsubscribe) перед каждым из наших .subscribe() который гарантирует, что все подписки будут очищены после уничтожения компонента.

Пример:

import { Component, OnDestroy, OnInit } from '@angular/core';
// RxJs 6.x+ import paths
import { filter, startWith, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { BookService } from '../books.service';

@Component({
    selector: 'app-books',
    templateUrl: './books.component.html'
})
export class BooksComponent implements OnDestroy, OnInit {
    private ngUnsubscribe = new Subject();

    constructor(private booksService: BookService) { }

    ngOnInit() {
        this.booksService.getBooks()
            .pipe(
               startWith([]),
               filter(books => books.length > 0),
               takeUntil(this.ngUnsubscribe)
            )
            .subscribe(books => console.log(books));

        this.booksService.getArchivedBooks()
            .pipe(takeUntil(this.ngUnsubscribe))
            .subscribe(archivedBooks => console.log(archivedBooks));
    }

    ngOnDestroy() {
        this.ngUnsubscribe.next();
        this.ngUnsubscribe.complete();
    }
}

Примечание. Важно добавить оператор takeUntil как последний, чтобы предотвратить утечки с промежуточными наблюдаемыми в цепочке операторов.

--- Изменить 2 (2016/12/28)

Источник 5

В учебнике Angular глава Routing теперь заявляет следующее: «Маршрутизатор управляет наблюдаемыми объектами, которые он предоставляет, и локализует подписки. Подписки очищаются при уничтожении компонента, защищая от утечек памяти, поэтому нам не нужно отписываться от параметры маршрута наблюдаемые. " - Марк Райкок

Вот discussion вопросов Github для Angular docs, касающихся Router Observables, где Уорд Белл упоминает, что прояснение всего этого находится в работе.

--- Редактировать 1

Источник 4

В этом видео от NgEurope Роб Вормальд также говорит, что вам не нужно отписываться от Router Observables. Он также упоминает службу http и ActivatedRoute.params в этом видео с ноября 2016 года .

--- Оригинальный ответ

TLDR:

Для этого вопроса существует (2) вида Observables - конечное значение и бесконечное значение.

Observables http генерируют конечные (1) значения, а что-то вроде event listener DOM. Observables производят бесконечные значения.

Если вы вручную вызываете subscribe (не используя асинхронный канал), то unsubscribe бесконечные Observables .

Не беспокойтесь о конечных , RxJs позаботятся о них.

Источник 1

Я нашел ответ от Роба Вормальда в Angular's Gitter here .

Он заявляет (я реорганизован для ясности и акцента мое)

если это однозначная последовательность (например, HTTP-запрос), ручная очистка не требуется (при условии, что вы подписываетесь в контроллере вручную)

я должен сказать "если это последовательность, которая завершается " (из которых однозначные последовательности, а-ля http, являются одной)

если это бесконечная последовательность , вы должны отписаться, что асинхронный канал делает для вас

Также он упоминает в этом видео на YouTube об Observables, которые they clean up after themselves ... в контексте Observables, которые complete (например, Promises, которые всегда выполняются, потому что они всегда производят 1 значение и заканчиваются - мы никогда не беспокоились о том, чтобы отписаться от Promises для убедитесь, что они убирают слушателей событий xhr , верно?).

Источник 2

Также в путеводителе по Rangle Angular 2 написано

В большинстве случаев нам не нужно явно вызывать метод отказа от подписки, если мы не хотим отменить досрочно, или если наш Observable имеет более длительный срок службы, чем наша подписка. Поведение операторов Observable по умолчанию заключается в удалении подписки сразу после публикации сообщений .complete () или .error (). Имейте в виду, что RxJS был разработан для того, чтобы использовать его в режиме «забей и забудь» большую часть времени.

Когда our Observable has a longer lifespan than our subscription действия фразы « our Observable has a longer lifespan than our subscription ?

Он применяется, когда подписка создается внутри компонента, который уничтожается до (или не «задолго») завершения Observable .

Я читаю это как значение, если мы подписываемся на http запрос или наблюдаемое, которое испускает 10 значений, и наш компонент уничтожается до того, как этот http запрос вернется или 10 значений будут отправлены, мы все еще в порядке!

Когда запрос вернется или, наконец, будет Observable 10-е значение, Observable будет завершен, и все ресурсы будут очищены.

Источник 3

Если мы посмотрим на этот пример из того же руководства по Rangle, то увидим, что для Subscription на route.params действительно требуется route.params unsubscribe() потому что мы не знаем, когда эти params перестанут изменяться (испуская новые значения).

Компонент может быть уничтожен путем перемещения в этом случае, и в этом случае параметры маршрута, вероятно, все еще будут изменяться (они могут технически измениться до завершения приложения), а ресурсы, выделенные в подписке, будут по-прежнему выделяться, поскольку не было выполнено completion .

Когда я должен хранить экземпляры Subscription и вызывать unsubscribe unsubscribe() в течение жизненного цикла NgOnDestroy, и когда я могу просто игнорировать их?

Сохранение всех подписок вносит много ошибок в код компонента.

HTTP Client Guide игнорирует такие подписки:

getHeroes() {
  this.heroService.getHeroes()
                   .subscribe(
                     heroes => this.heroes = heroes,
                     error =>  this.errorMessage = <any>error);
}

В то же время в Route & Navigation Guide говорится, что:

В конце концов мы перейдем куда-нибудь еще. Маршрутизатор удалит этот компонент из DOM и уничтожит его. Мы должны убраться за собой, прежде чем это произойдет. В частности, мы должны отписаться, прежде чем Angular уничтожит компонент. Невыполнение этого требования может привести к утечке памяти.

Мы отписываемся от нашего Observable в методе ngOnDestroy .

private sub: any;

ngOnInit() {
  this.sub = this.route.params.subscribe(params => {
     let id = +params['id']; // (+) converts string 'id' to a number
     this.service.getHero(id).then(hero => this.hero = hero);
   });
}

ngOnDestroy() {
  this.sub.unsubscribe();
}

В случае необходимости отписки можно использовать следующий оператор для метода наблюдаемой трубы

import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { OnDestroy } from '@angular/core';

export const takeUntilDestroyed = (componentInstance: OnDestroy) => <T>(observable: Observable<T>) => {
  const subjectPropertyName = '__takeUntilDestroySubject__';
  const originalOnDestroy = componentInstance.ngOnDestroy;
  const componentSubject = componentInstance[subjectPropertyName] as Subject<any> || new Subject();

  componentInstance.ngOnDestroy = (...args) => {
    originalOnDestroy.apply(componentInstance, args);
    componentSubject.next(true);
    componentSubject.complete();
  };

  return observable.pipe(takeUntil<T>(componentSubject));
};

это можно использовать так:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Component({ template: '<div></div>' })
export class SomeComponent implements OnInit, OnDestroy {

  ngOnInit(): void {
    const observable = Observable.create(observer => {
      observer.next('Hello');
    });

    observable
      .pipe(takeUntilDestroyed(this))
      .subscribe(val => console.log(val));
  }

  ngOnDestroy(): void {
  }
}

Оператор переносит метод компонента ngOnDestroy.

Важно: оператор должен быть последним в наблюдаемой трубе.


Для обработки подписки я использую класс «Unsubscriber».

Вот класс Unsubscriber.

export class Unsubscriber implements OnDestroy {
  private subscriptions: Subscription[] = [];

  addSubscription(subscription: Subscription | Subscription[]) {
    if (Array.isArray(subscription)) {
      this.subscriptions.push(...subscription);
    } else {
      this.subscriptions.push(subscription);
    }
  }

  unsubscribe() {
    this.subscriptions
      .filter(subscription => subscription)
      .forEach(subscription => {
        subscription.unsubscribe();
      });
  }

  ngOnDestroy() {
    this.unsubscribe();
  }
}

И Вы можете использовать этот класс в любом компоненте / Сервис / Эффект и т.д.

Пример:

class SampleComponent extends Unsubscriber {
    constructor () {
        super();
    }

    this.addSubscription(subscription);
}

Еще одно короткое дополнение к вышеупомянутым ситуациям:

  • Всегда отменяйте подписку, если новые значения в подписанном потоке больше не требуются или не имеют значения, это приведет к уменьшению количества триггеров и увеличению производительности в нескольких случаях. Такие примеры, как компоненты, в которых подписанные данные / события больше не существуют или требуется новая подписка на весь новый поток (обновление и т. Д.), Являются хорошим примером для отказа от подписки.

Обычно вам нужно отписаться, когда компоненты будут уничтожены, но Angular будет обрабатывать их все больше и больше, например, в новой минорной версии Angular4 у них есть этот раздел для отмены подписки:

Вам нужно отписаться?

Как описано в разделе «ActivatedRoute: универсальный магазин для информации о маршруте» на странице «Маршрутизация и навигация», маршрутизатор управляет наблюдаемыми объектами и локализует подписки. Подписки очищаются при уничтожении компонента, защищая от утечек памяти, поэтому вам не нужно отписываться от маршрута paramMap Observable.

Также приведенный ниже пример является хорошим примером из Angular для создания компонента и его уничтожения после, посмотрите, как компонент реализует OnDestroy. Если вам нужен onInit, вы также можете реализовать его в своем компоненте, например, как OnInit, OnDestroy

import { Component, Input, OnDestroy } from '@angular/core';  
import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs/Subscription';

@Component({
  selector: 'my-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        [disabled]="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})

export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;

  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }

  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }

  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

После ответа @seangwright я написал абстрактный класс, который обрабатывает «бесконечные» подписки наблюдаемых в компонентах:

import { OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { PartialObserver } from 'rxjs/Observer';

export abstract class InfiniteSubscriberComponent implements OnDestroy {
  private onDestroySource: Subject<any> = new Subject();

  constructor() {}

  subscribe(observable: Observable<any>): Subscription;

  subscribe(
    observable: Observable<any>,
    observer: PartialObserver<any>
  ): Subscription;

  subscribe(
    observable: Observable<any>,
    next?: (value: any) => void,
    error?: (error: any) => void,
    complete?: () => void
  ): Subscription;

  subscribe(observable: Observable<any>, ...subscribeArgs): Subscription {
    return observable
      .takeUntil(this.onDestroySource)
      .subscribe(...subscribeArgs);
  }

  ngOnDestroy() {
    this.onDestroySource.next();
    this.onDestroySource.complete();
  }
}

Чтобы использовать его, просто расширьте его в своем угловом компоненте и вызовите subscribe() метод следующим образом:

this.subscribe(someObservable, data => doSomething());

Он также принимает ошибку и завершает обратные вызовы, как обычно, объект-наблюдатель или вообще не обратные вызовы. Не забудьте вызвать, super.ngOnDestroy() если вы также реализуете этот метод в дочернем компоненте.

Найдите здесь дополнительную ссылку Бена Леша: medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 .


в SPA применения при ngOnDestroy функции (угловой) Жизненным Циклом Для каждой подписки вы должны отказаться его. преимущество => чтобы государство не стало слишком тяжелым.

например: в компоненте 1:

import {UserService} from './user.service';

private user = {name: 'test', id: 1}

constructor(public userService: UserService) {
    this.userService.onUserChange.next(this.user);
}

в сервисе:

import {BehaviorSubject} from 'rxjs/BehaviorSubject';

public onUserChange: BehaviorSubject<any> = new BehaviorSubject({});

в компоненте 2:

import {Subscription} from 'rxjs/Subscription';
import {UserService} from './user.service';

private onUserChange: Subscription;

constructor(public userService: UserService) {
    this.onUserChange = this.userService.onUserChange.subscribe(user => {
        console.log(user);
    });
}

public ngOnDestroy(): void {
    // note: Here you have to be sure to unsubscribe to the subscribe item!
    this.onUserChange.unsubscribe();
}

Вам не нужно иметь кучу подписок и отписаться вручную. Используйте Subject и takeUntil combo для обработки подписок как босс:

import {Subject} from "rxjs/Subject";

@Component(
    {
        moduleId: __moduleName,
        selector: 'my-view',
        templateUrl: '../views/view-route.view.html',
    }
)
export class ViewRouteComponent implements OnDestroy
{
    componentDestroyed$: Subject<boolean> = new Subject();

    constructor(protected titleService: TitleService)
    {
        this.titleService.emitter1$
            .takeUntil(this.componentDestroyed$)
            .subscribe(
            (data: any) =>
            {
                // ... do something 1
            }
        );

        this.titleService.emitter2$
            .takeUntil(this.componentDestroyed$)
            .subscribe(
            (data: any) =>
            {
                // ... do something 2
            }
        );

        // ...

        this.titleService.emitterN$
            .takeUntil(this.componentDestroyed$)
            .subscribe(
            (data: any) =>
            {
                // ... do something N
            }
        );
    }

    ngOnDestroy()
    {
        this.componentDestroyed$.next(true);
        this.componentDestroyed$.complete();
    }
}

Альтернативный подход , который был предложен @acumartini в комментариях , использует takeWhile вместо takeUntil . Вы можете предпочесть это, но имейте в виду, что таким образом выполнение Observable не будет отменено для ngDestroy вашего компонента (например, когда вы выполняете трудоемкие вычисления или ждете данных с сервера). Метод, основанный на takeUntil , не имеет этого недостатка и приводит к немедленной отмене запроса. Спасибо @AlexChe за подробное объяснение в комментариях .

Итак, вот код:

@Component(
    {
        moduleId: __moduleName,
        selector: 'my-view',
        templateUrl: '../views/view-route.view.html',
    }
)
export class ViewRouteComponent implements OnDestroy
{
    alive: boolean = true;

    constructor(protected titleService: TitleService)
    {
        this.titleService.emitter1$
            .takeWhile(() => this.alive)
            .subscribe(
            (data: any) =>
            {
                // ... do something 1
            }
        );

        this.titleService.emitter2$
            .takeWhile(() => this.alive)
            .subscribe(
            (data: any) =>
            {
                // ... do something 2
            }
        );

        // ...

        this.titleService.emitterN$
            .takeWhile(() => this.alive)
            .subscribe(
            (data: any) =>
            {
                // ... do something N
            }
        );
    }

    // Probably, this.alive = false MAY not be required here, because
    // if this.alive === undefined, takeWhile will stop. I
    // will check it as soon, as I have time.
    ngOnDestroy()
    {
        this.alive = false;
    }
}

Основано на: Использование наследования классов для привязки к жизненному циклу компонента Angular 2

Еще один общий подход:

export abstract class UnsubscribeOnDestroy implements OnDestroy {
  protected d$: Subject<any>;

  constructor() {
    this.d$ = new Subject<void>();

    const f = this.ngOnDestroy;
    this.ngOnDestroy = () => {
      f();
      this.d$.next();
      this.d$.complete();
    };
  }

  public ngOnDestroy() {
    // no-op
  }

}

И использовать:

@Component({
    selector: 'my-comp',
    template: ``
})
export class RsvpFormSaveComponent extends UnsubscribeOnDestroy implements OnInit {

    constructor() {
        super();
    }

    ngOnInit(): void {
      Observable.of('bla')
      .takeUntil(this.d$)
      .subscribe(val => console.log(val));
    }
}


Официальная документация Angular 2 дает объяснение, когда отписаться и когда ее можно безопасно игнорировать. Посмотрите на эту ссылку:

https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

Найдите абзац с заголовком « Родитель и дети общаются через службу», а затем синее поле:

Обратите внимание, что мы фиксируем подписку и отменяем подписку, когда AstronautComponent уничтожен. Это шаг защиты от утечки памяти. В этом приложении нет никакого реального риска, потому что время жизни AstronautComponent совпадает со временем жизни самого приложения. Это не всегда верно в более сложных приложениях.

Мы не добавляем эту защиту в MissionControlComponent, потому что, как родитель, он контролирует время жизни MissionService.

Я надеюсь, это поможет вам.


По-разному. Если, вызывая someObservable.subscribe() , вы начинаете задерживать некоторый ресурс, который должен быть вручную освобожден, когда жизненный цикл вашего компонента закончен, вам следует вызвать theSubscription.unsubscribe() чтобы предотвратить утечку памяти.

Давайте внимательнее посмотрим на ваши примеры:

getHero() возвращает результат http.get() . Если вы посмотрите на исходный код angular 2, http.get() создаст два прослушивателя событий:

_xhr.addEventListener('load', onLoad);
_xhr.addEventListener('error', onError);

и, вызвав unsubscribe() , вы можете отменить запрос и слушателей:

_xhr.removeEventListener('load', onLoad);
_xhr.removeEventListener('error', onError);
_xhr.abort();

Обратите внимание, что _xhr зависит от платформы, но я думаю, можно с уверенностью предположить, что это XMLHttpRequest() в вашем случае.

Обычно этого достаточно для того, чтобы гарантировать ручной вызов unsubscribe() . Но в соответствии с этой спецификацией WHATWG XMLHttpRequest() подлежит сборке мусора после того, как он «сделан», даже если к нему подключены прослушиватели событий. Так что, я думаю, именно поэтому в angular 2 официальное руководство опускает unsubscribe() и позволяет GC очистить слушателей.

Что касается вашего второго примера, это зависит от реализации params . На сегодняшний день в угловом официальном путеводителе больше не отображается отписка от params . Я снова заглянул в src и обнаружил, что params - просто BehaviorSubject . Поскольку ни прослушиватели событий, ни таймеры не использовались, а глобальные переменные не создавались, можно с уверенностью опустить unsubscribe() .

Суть вопроса в том, что всегда вызывайте unsubscribe() для защиты от утечки памяти, если только вы не уверены, что выполнение наблюдаемого не создает глобальные переменные, не добавляет прослушиватели событий, не устанавливает таймеры и не делает ничего другого, что приводит к в утечках памяти.

Если есть сомнения, посмотрите на реализацию этого наблюдаемого. Если наблюдаемая записала некоторую логику очистки в unsubscribe() , которая обычно является функцией, возвращаемой конструктором, у вас есть веская причина серьезно подумать о вызове unsubscribe() .


Поскольку решение seangwright (Edit 3) кажется очень полезным, я также счел затруднительным упаковать эту функцию в базовый компонент и подсказал другим товарищам по команде проекта не забывать вызывать super () для ngOnDestroy для активации этой функции.

Этот ответ предоставляет способ освободиться от супер-вызова и сделать «componentDestroyed $» ядром базового компонента.

class BaseClass {
    protected componentDestroyed$: Subject<void> = new Subject<void>();
    constructor() {

        /// wrap the ngOnDestroy to be an Observable. and set free from calling super() on ngOnDestroy.
        let _$ = this.ngOnDestroy;
        this.ngOnDestroy = () => {
            this.componentDestroyed$.next();
            this.componentDestroyed$.complete();
            _$();
        }
    }

    /// placeholder of ngOnDestroy. no need to do super() call of extended class.
    ngOnDestroy() {}
}

И тогда вы можете свободно использовать эту функцию, например:

@Component({
    selector: 'my-thing',
    templateUrl: './my-thing.component.html'
})
export class MyThingComponent extends BaseClass implements OnInit, OnDestroy {
    constructor(
        private myThingService: MyThingService,
    ) { super(); }

    ngOnInit() {
        this.myThingService.getThings()
            .takeUntil(this.componentDestroyed$)
            .subscribe(things => console.log(things));
    }

    /// optional. not a requirement to implement OnDestroy
    ngOnDestroy() {
        console.log('everything works as intended with or without super call');
    }

}




angular-component-life-cycle