angular
April 4

Почему важно отписываться

Разберем какие проблемы могут возникнуть если забыть отписаться от RxJS потока.

Для начала создадим реалистичный пример с использованием Angular и Akita.

Сначала создадим простое хранилище Akita, которое содержит одно свойство:

export interface DemoState {
  someProp: number;
}

export function createInitialState(): DemoState {
  return {
    someProp: 0
  };
}

@StoreConfig({ name: 'demo' })
export class DemoStore extends Store<DemoState> {
  constructor() {
    super(createInitialState());
  }
}
export class DemoQuery extends Query<DemoState> {
  selectSomeProp$ = this.select(state => state.someProp);

  constructor(protected store: DemoStore) {
    super(store);
  }
}

Отлично. Теперь давайте создадим два компонента. Первый будет отслеживать ключ someProp хранилища, а второй компонент будет обновлять его.

export class PageOneComponent {
  subscription: Subscription;

  constructor(private demoQuery: DemoQuery) {}

  ngOnInit() {
    this.subscription = this.demoQuery.selectSomeProp$.subscribe(prop => {
      this.someMethod();
    });
  }

  someMethod() {}
}
@Component({
  template: `<button (click)="update()">Update prop</button>`
})
export class PageTwoComponent {
  constructor(private demoService: DemoService) {}

  update() {
    this.demoService.updateKey();
  }
}


1. Утечка памяти

Функция подписки сохраняет ссылку на this (экземпляр компонента), поэтому экземпляр PageOne недоступен для сборки мусора и не будет удален. В Chrome DevTools можно отследить проблему следующим образом:

Как видите, экземпляр компонента все еще находится в памяти, даже если Angular уничтожил компонент при переключении таба.

Проблема два — Неожиданное поведение

Предположим, нам нужно вызвать метод detectChanges() при обновлении someProp.

ngOnInit() {
  this.subscription = this.demoQuery.selectSomeProp$.subscribe(prop => {
    this.someMethod();
    this.cdr.detectChanges();
  });
}

Теперь перейдем к компоненту PageTwoComponent, обновим хранилище и посмотрим, что происходит:

Мы не очистили нашу функцию подписки перед тем, как Angular уничтожил компонент, поэтому при обновлении хранилища она запускается и вызывает метод detectChanges() на компоненте, который с точки зрения Angular уже уничтожен.

Решение одно - делать отписки

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

начиная с Angular 16 был добавлен оператор takeUntilDestroy() который берет на себя большую часть работы связанной с отписками. Я писал про него ранее.

После отписки Javascript сможет удалять из памяти destroyed-компоненты, и профайлер будет выглядеть так:

Это вольный перевод материала