Подписка на сервис наблюдаемые излучает даже после разрушения компонента

У меня проблемы с наблюдаемым в моем сервисе. Следующий код иллюстрирует это:

@Injectable({
  providedIn: 'root'
})
export class MyService {
  public globalVariable: BehaviorSubject<string> = new BehaviorSubject('');
}

У меня есть компонент функции:

export class ComponentA implements OnInit {
   constructor(public myService : MyService ) {
      this.myService.globalVariable.next('newValue');
   }

   ngOnInit() {
      this.myService.globalVariable.subscribe(_ => console.log('=> hello'));
   }
}

Модуль приложения:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ComponentAModule,
    ComponentBModule,
    AppRoutingModule
  ],
  providers: [MyService],
  bootstrap: [AppComponent]
})
export class AppModule {
}

И, наконец, структура проекта:

app-module.ts
app-routing.module.ts
-components
-- componentA
--- componentA.module.ts
--- componentA-routing.module.ts
--- componentA.component.ts
--- componentA.component.html
-- componentB
--- componentB.module.ts
--- componentB-routing.module.ts
--- componentB.component.ts
--- componentB.component.html

Теперь проблема, с которой я столкнулся, заключается в том, что когда я перехожу к componentA, вывод будет:

=> hello
=> hello

До сих пор все в норме и ведет себя так, как я ожидал. Запускается первая подписка, а затем изменение globalVariable конструктором componentA.

Однако, когда я перехожу к componentB и возвращаюсь к componentA, вывод будет:

=> hello
=> hello
=> hello

Он добавляет один каждый раз, когда я возвращаюсь к componentA. Как будто он создает новый экземпляр MyService? Или не уничтожает подписку при выходе?

Информация: ленивой загрузки нет.


person lolplayer101    schedule 25.02.2019    source источник


Ответы (3)


Подписки должны быть уничтожены вручную, если они не обрабатываются самим Angular. В основном это относится ко всем имеющимся у вас подпискам httpClient. Если вы, например, используйте канал | async, Angular позаботится об отписке.

Вызов yourSubscription.unsubscribe() в ngOnDestroy() вашего компонента.

Обычно я создаю BaseComponent, который отменяет подписку за меня. Используйте приведенный ниже класс во всех своих компонентах, расширив его. Оберните каждый вызов подписки в super.addSubscription()

import { OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

/**
 * This class handles the subscribing and unsubscribing of subscriptions to avoid memory leaks
 * and can be inherited by members
 *
 * @export
 */
export abstract class BaseComponent implements OnDestroy {

private subscriptions: Subscription[] = new Array<Subscription>();

ngOnDestroy() {
    this.removeSubscriptions();
}

/**
 * Adds a subscriptions so it can be deleted in ngOnDestroy
 *
 * @param subscription The subscription that should be added
 * @memberof BaseComponent
 */
protected addSubscription(subscription: Subscription) {
    this.subscriptions.push(subscription);
}

/**
 * Unsubscribes from any open subscriptions in the subscriptions array in ngOnDestroy
 *
 * @memberof AbstractBaseComponent
 */
private removeSubscriptions() {
    for (let subscription of this.subscriptions) {
        subscription.unsubscribe();
    }
}
}

ОБНОВЛЕНИЕ

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

export class ComponentA extends BaseComponent implements OnInit {
    constructor(public myService : MyService ) {
       this.myService.globalVariable.next('newValue');
    }
    ngOnInit() {
       super.addSubscription(
           this.myService.globalVariable.subscribe(_ => console.log('=> hello'))
       )
    }
}
person Julien Ambos    schedule 25.02.2019
comment
вот о чем я думал, но потом мне стало интересно, почему мне не нужно делать это для вызовов httpClient. пример: this.myService.getDataX (). subscribe (_ = ›doSomething ()); ? - person lolplayer101; 25.02.2019
comment
Я понимаю, как работает ваш подход, но это не объясняет, почему мы никогда не отказываемся от подписки на вызовы httpClient, как в руководствах по angular: angular.io/tutorial/toh-pt6 - person lolplayer101; 25.02.2019
comment
Потому что это просто не часть процесса обучения и может запутать учащихся с жизненными циклами и утечками! Тем не менее, работает ли мой ответ на вас? Кроме того, в учебнике иногда используется канал async, и, как упоминалось в моем ответе, Angular автоматически обрабатывает отказ от подписки, если вы используете канал async. - person Julien Ambos; 25.02.2019
comment
Да, большое спасибо. итак последний вопрос. можно ли мне также отказаться от подписки на звонки httpClient? (Теперь я знаю, что должен сделать это для атрибутов Observable в службе) - person lolplayer101; 25.02.2019
comment
Нет, не делайте это в сервисе, а делайте это в компоненте, который использует сервис! Есть одно правило: если вы набираете .subscribe() всегда, заключите его в super.addSubscription() (как упоминалось в моем ответе) - person Julien Ambos; 25.02.2019
comment
Я знаю, что должен сделать это в компоненте, я просто хотел знать, применяется ли отказ от подписки к вызовам выборки данных службы. пример: super.addSubscription (this.myService.getData (). subscribe (_ = ›something ())) - person lolplayer101; 25.02.2019

Вам нужно unsubscribe внутри ngOnDestroy:

import { Subscription } from 'rxjs';

globalVariable$: Subscription;

ngOnInit() {
  this.globalVariable$ = this.myService.globalVariable.subscribe(_ => console.log('=> hello'));
}

ngOnDestroy() {
  this.globalVariable$.unsubscribe();
}
person Vadi    schedule 25.02.2019
comment
вот о чем я думал, но потом мне стало интересно, почему мне не нужно делать это для вызовов httpClient. пример: this.myService.getDataX (). subscribe (_ = ›doSomething ()); ? - person lolplayer101; 25.02.2019
comment
@ lolplayer101, если вы подписываетесь таким образом в жизненном крючке компонента или службы this.myService.getDataX().subscribe(_ => doSomething()); , you need to unsubscribe using ngOnDestroy`. Выходить из подписки после уничтожения - плохая практика. Если вы используете только асинхронный канал в шаблоне для подписки, он автоматически откажется от подписки. - person Vadi; 25.02.2019

Если вы хотите использовать подписку, а не асинхронный канал, вы можете использовать оператор RxJs takeWhile. Пожалуйста, смотрите код ниже ...

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit, OnDestroy {
  name = 'Angular';
  isActive: boolean;
  // returned from service.  Does not need to initialized here
  thingToSubscribeTo:Observable<any> = new Observable<any>();

  ngOnInit() {
    this.isActive = true;
    // you can replace with call to service then pipe it.
    this.thingToSubscribeTo.pipe(
      map(res => {
        // handle subscription
      }),
      takeWhile(() => this.isActive)
    );

  }

  ngOnDestroy() {
    this.isActive = false;
  }
}
person Paul    schedule 25.02.2019
comment
Технически takeUntil - это не то же самое, что unsubscribe, потому что unsubscribe уничтожает подписку, а takeUntil завершает ее: reactivex.io/rxjs/class/es6/ - person Julien Ambos; 25.02.2019
comment
Спасибо Жюльен, это полезная информация. Я считаю, что решение takeUntil также должно решить эту проблему. Однако принятое решение может быть лучше, поскольку оно не требует реализации takeUntil для каждого компонента, имеющего подписки, на которые необходимо отменить подписку. - person Paul; 25.02.2019
comment
Привет, Пол, извини, если мой комментарий получился неправильным. Ваш подход абсолютно верен. Я просто хотел отметить, что технически не то же самое, что информировать будущих читателей о разнице. - person Julien Ambos; 25.02.2019