Кутові / RxJs Коли мені слід скасувати підписку на "Підписка"


719

Коли я повинен зберігати Subscription екземпляри та викликати unsubscribe()протягом життєвого циклу NgOnDestroy і коли я можу їх просто ігнорувати?

Збереження всіх підписок вводить багато безладу в компонентний код.

Посібник для клієнтів HTTP ігнорує такі підписки:

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

У той же час керівництво по маршруту та навігації говорить, що:

Врешті-решт ми перейдемо кудись ще. Маршрутизатор видалить цей компонент з 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();
}

20
Я думаю, що Subscriptions http-requestsможна ігнорувати, оскільки вони дзвонять лише onNextодин раз і потім дзвонять onComplete. RouterЗамість викликів onNextкілька разів і ніколи не може назвати onComplete(не впевнений , що ...). Те саме стосується і Observables Events. Тому я думаю, що це має бути unsubscribed.
Спрингрбуа

1
@ gt6707a Потік завершується (або не завершується) незалежно від будь-якого спостереження за цим завершенням. Відкликані дзвінки (спостерігач), надані функції підписки, не визначають, чи розподіляються ресурси. Саме заклик до subscribeсебе потенційно розподіляє ресурси за течією.
seangwright

Відповіді:


980

--- Редагувати 4 - Додаткові ресурси (2018/09/01)

В нещодавньому епізоді Пригоди в кутовому Бен Леш та Уорд Белл обговорюють проблеми навколо того, як / коли відписатися на підписку на компонент. Обговорення розпочинається близько 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 .

У відповідь Бен згадує, що зараз ведуться дискусії, щоб дозволити спостерігачам підключитися до подій життєвого циклу компонентів углових компонентів, а Уорд пропонує «Спостережувані події життєвого циклу», на які компонент міг би підписатись як спосіб знати, коли потрібно виконати Спостереження, що підтримуються як внутрішній стан компонента.

Зважаючи на це, нам зараз потрібні рішення, тому ось деякі інші ресурси.

  1. Рекомендація щодо takeUntil()шаблону від основної команди команди RxJs Ніколаса Джеймісона та правило tslint, яке допоможе виконувати його. https://ncjamieson.com/avoiding-takeuntil-leaks/

  2. Легкий пакет npm, який відкриває оператор "Спостереження", який приймає екземпляр компонента ( 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 і презентаційні компоненти повинні бути розглянуті. Потім ви можете | asyncспостерігати з компонента Feature в компонент InputPresentational, що означає, що ніде не потрібно підписки. Детальніше про цей підхід читайте тут

--- Редагувати 3 - "Офіційне" рішення (2017/04/09)

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

Ми повинні використовувати все рішення вперед - додати private ngUnsubscribe = new Subject();поле до всіх компонентів, які мають .subscribe()дзвінки до Observables у коді свого класу.

Потім ми закликаємо 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 (28.12.2016)

Джерело 5

У кутовому посібнику, в главі маршрутизації, зараз зазначено наступне: "Маршрутизатор управляє спостереженнями, які він надає, і локалізує підписки. Підписки очищаються при знищенні компонента, захищаючи від витоків пам'яті, тому нам не потрібно скасовувати підписку на маршрут парами Спостерігається ". - Марк Райкок

Ось дискусія щодо питань Github для кутових документів щодо маршрутизаційних спостережень, де Уорд Белл згадує, що роз'яснення щодо всього цього є у творах.

--- Редагувати 1

Джерело 4

У цьому відео від NgEurope Роб Вормалд також говорить, що вам не потрібно скасовувати підписку на Router Observables. Він також згадує про httpслужбу і ActivatedRoute.paramsв цьому відео з листопада 2016 року .

--- Оригінальна відповідь

TLDR:

Для цього питання існує (2) види Observables- кінцеве значення і нескінченне значення.

http Observablesвиробляють кінцеві (1) значення і щось на зразок DOM event listener Observablesвиробляють нескінченні значення.

Якщо ви вручну викликати subscribe(не використовуючи асинхронну трубку), то unsubscribeз нескінченного Observables .

Не турбуйтеся про кінцеві , RxJsвони подбають про них.

Джерело 1

Я розшукав відповідь від Rob Wormald в GITTER кутового в тут .

Він констатує (я реорганізований для ясності, і акцент - мій)

якщо його однозначна послідовність (наприклад, запит http), ручне очищення не є необхідним (якщо припустити, що ви підписалися на контролер вручну)

я повинен сказати, "якщо її послідовність, яка завершує " (з яких послідовності з одним значенням, a la http, є одна)

якщо це нескінченна послідовність , вам слід скасувати підписку, що робить труба асинхронізації для вас

Крім того, він згадує у цьому відео на YouTube " Оглядомі", що they clean up after themselves... в контексті "Спостережуваних", які complete(як Обіцянки, які завжди завершуються, оскільки вони завжди створюють 1 цінність і закінчуються) - ми ніколи не турбувалися про скасування підписки на Обіцяння, щоб переконатися, що вони очистять xhrподію слухачів, правда?).

Джерело 2

Також у посібнику з Rangle до кута 2 він читається

У більшості випадків нам не потрібно буде чітко закликати метод скасування підписки, якщо ми не хочемо скасувати достроково або наш Спостережуваний довший термін служби, ніж наша підписка. Поведінка операторів, які спостерігаються за замовчуванням, - розпоряджатися підпискою, як тільки публікуються повідомлення .complete () або .error (). Майте на увазі, що RxJS був розроблений таким чином, щоб він використовувався "вогонь і забудь" більшу частину часу.

Коли словосполучення our Observable has a longer lifespan than our subscription застосовується?

Він застосовується, коли підписка створена всередині компонента, який знищується до Observableзавершення (або не задовго до цього) .

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

Коли запит повернеться або 10-е значення нарешті випромінюється Observable, завершиться і всі ресурси будуть очищені.

Джерело 3

Якщо ми подивимось на цей приклад з того ж посібника з Rangle, то можемо побачити, що Subscriptionдо route.paramsцього потрібно, unsubscribe()оскільки ми не знаємо, коли paramsвони перестануть змінюватися (випромінюючи нові значення).

Компонент може бути знищений за допомогою навігації, і в цьому випадку парами маршруту, ймовірно, все ж змінюватимуться (технічно вони можуть змінюватися, поки додаток не закінчиться), а ресурси, виділені в підписці, все одно будуть виділятися, оскільки цього не було completion.


15
complete()Сам дзвінок , здається, не очищає передплати. Однак виклик next()і потім complete()робить, я вважаю, що takeUntil()зупиняється лише тоді, коли створюється значення, а не тоді, коли послідовність закінчується.
Firefly

2
@seangwright Швидкий тест з членом типу Subjectвсередині компонента та включення його ngIfдо запуску ngOnInitта ngOnDestroyпоказує, що суб'єкт та його підписки ніколи не завершаться або не будуть утилізовані (підключено finallyоператора до підписки). Я повинен зателефонувати Subject.complete()в ngOnDestroy, так підписки можна прибирати за собою.
Ларс

3
Ваш --- Edit 3 дуже проникливий, дякую! У мене просто виникає запитання: якщо, використовуючи takeUnitlпідхід, нам ніколи не потрібно вручну скасовувати підписку на спостереження? Це так? Крім того, чому нам потрібно дзвонити next()в ngOnDestroy, чому не просто дзвонити complete()?
потворний код

6
@seangwright Це невтішно; додаткова котельня прикро.
губчастий смокч

3
Правка 3 обговорена в контексті подій на medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87
HankCa

89

Вам не потрібно мати купу підписок і скасувати передплату вручну. Використовуйте комбінацію Subject і takeUntil для обробки підписок, як бос:

import { Subject } from "rxjs"
import { takeUntil } from "rxjs/operators"

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

  constructor(private titleService: TitleService) {}

  ngOnInit() {
    this.titleService.emitter1$
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe((data: any) => { /* ... do something 1 */ })

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

    //...

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

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

Альтернативний підхід , запропонований @acumartini у коментарях , використовує takeWhile замість takeUntil . Ви можете віддати перевагу, але зауважте, що таким чином ваше спостережуване виконання не буде скасовано на ngDestroy вашого компонента (наприклад, коли ви робите трудомісткі розрахунки або чекаєте даних з сервера). Метод, заснований на takeUntil , не має цього недоліку і призводить до негайного скасування запиту. Дякуємо @AlexChe за детальне пояснення в коментарях .

Отже ось код:

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

  constructor(private titleService: TitleService) {}

  ngOnInit() {
    this.titleService.emitter1$
      .pipe(takeWhile(() => this.alive))
      .subscribe((data: any) => { /* ... do something 1 */ })

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

    // ...

    this.titleService.emitterN$
      .pipe(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
  }
}

1
Якщо він просто використовує bool для утримання держави, як зробити так, щоб "takeUntil" працював так, як очікувалося?
Валь

5
Я думаю, що між використанням takeUntilта використанням існує значна різниця takeWhile. Перший скасовує підписку на джерело, що спостерігається негайно при звільненні, тоді як останнє скасовує підписку лише після отримання наступного значення джерелом, яке можна спостерігати. Якщо створення значення спостережуваним джерелом є операцією, що вимагає ресурсів, вибір між ними може виходити за межі переваг стилю. Дивіться планку
Алекс Че

1
Дякуємо @AlexChe за надану цікаву планку! Це дуже справедливий момент для загального використання takeUntilvs takeWhile, однак, не для нашого конкретного випадку. Коли нам потрібно скасувати передплату слухачів на знищення компонентів , ми просто перевіряємо булеве значення, як () => aliveу takeWhile, тому будь-які операції, що споживають час / пам'ять, не використовуються, і різниця значною мірою стосується стилізації (ofc, для цього конкретного випадку).
метамейкер

1
@metamaker Скажімо, у нашому компоненті ми підписуємось на підрозділ Observable, який внутрішньо видобуває криптовалюту і запускає nextподію за кожну видобуту монету, а видобуток однієї такої монети займає день. З takeUntilми будемо відмовитися від видобутку джерела Observableвідразу ж після ngOnDestroyвикликаються під час нашого компонента руйнування. Таким чином, Observableфункція видобутку здатна негайно скасувати свою роботу під час цього процесу.
Алекс Че

1
OTOH, якщо ми використовуємо takeWhile, ngOnDestoryми просто встановлюємо булеву змінну. Але Observableфункція майнінгу може все ще працювати до одного дня, і лише тоді під час nextдзвінка вона зрозуміє, що підписки немає активних і її потрібно скасувати.
Алекс Че

75

Клас передплати має цікаву особливість:

Представляє одноразовий ресурс, такий як виконання спостережуваного. У підписки є один важливий метод - скасувати підписку, який не бере аргументів і просто розпоряджається ресурсом, який зберігається підпискою.
Крім того, підписки можуть бути згруповані разом методом add (), який додасть дочірню підписку до поточної підписки. Якщо підписка скасована, всі її діти (та її онуки) також будуть скасовані.

Ви можете створити сукупний об’єкт підписки, який групує всі ваші підписки. Ви робите це, створюючи порожню підписку та додаючи до неї підписки, використовуючи її add()метод. Коли ваш компонент знищений, вам потрібно лише скасувати підписку на сукупну підписку.

@Component({ ... })
export class SmartComponent implements OnInit, OnDestroy {
  private subscriptions = new Subscription();

  constructor(private heroService: HeroService) {
  }

  ngOnInit() {
    this.subscriptions.add(this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes));
    this.subscriptions.add(/* another subscription */);
    this.subscriptions.add(/* and another subscription */);
    this.subscriptions.add(/* and so on */);
  }

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

Я використовую такий підхід. Цікаво, чи це краще, ніж використовувати підхід з takeUntil (), як у прийнятій відповіді .. недоліки?
Мануель Ді Іоріо

Жодних недоліків, про які я знаю. Я не думаю, що це краще, просто інше.
Стівен Лікенс

2
Див. Medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 для подальшої дискусії щодо офіційного takeUntilпідходу проти такого підходу збору підписок та дзвінків unsubscribe. (Цей підхід здається мені набагато чистішим.)
Джош Келлі

3
Одна невелика користь у цій відповіді: вам не доведеться перевіряти, чи this.subscriptionsє нульовим
user2023861,

1
Просто уникайте ланцюжка методів додавання, як, наприклад, sub = subsciption.add(..).add(..)тому що в багатьох випадках це дає несподівані результати github.com/ReactiveX/rxjs/isissue/2769#issuecomment-345636477
Євгеній

32

Деякі найкращі практики щодо скасування підписки, що спостерігаються всередині кутових компонентів:

Цитата від Routing & Navigation

Підписавшись на спостережуваний компонент, ви майже завжди домовляєтесь скасувати підписку, коли компонент знищений.

Є кілька виняткових спостережень, де це не потрібно. З-поміж винятків є спостережувані дані ActivateRoute.

ActivateRoute та його спостереження ізолюються від самого маршрутизатора. Маршрутизатор знищує маршрутизований компонент, коли він більше не потрібен, і введений ActivateRoute гине з ним.

Сміливо скасуйте підписку. Це нешкідливо і ніколи не є поганою практикою.

І у відповідь на наступні посилання:

Я зібрав декілька найкращих практик щодо скасування підписок на видимість у внутрішніх компонентах Angular, щоб поділитися з вами:

  • httpвідстежувана підписка є умовною, і ми повинні враховувати ефекти запущеного зворотного дзвінка підписки після того, як компонент знищується в кожному конкретному випадку. Ми знаємо, що кутовий відписує і очищає httpспостережуване (1) , (2) . Хоча це правда з точки зору ресурсів, це лише половина історії. Скажімо, ми говоримо про прямі дзвінки httpвсередині компонента таhttp відповідь зайняла більше часу, ніж потрібно, щоб користувач закрив компонент. Thesubscribe()обробник все ще буде викликатися, навіть якщо компонент закритий і знищений. Це може мати небажані побічні ефекти, а в гірших сценаріях залишати стан програми порушеним. Це також може спричинити винятки, якщо код у зворотному дзвінку намагається викликати щось, щойно було видалено. Однак при цьому вони час від часу бажані. Скажімо, ви створюєте клієнт електронної пошти, і ви викликаєте звук, коли електронна пошта закінчується надсиланням - добре, ви все одно хочете, щоб це сталося, навіть якщо компонент закритий ( 8 ).
  • Не потрібно скасовувати підписку на спостереження, які завершуються або помиляються. Однак шкоди в цьому немає (7) .
  • Використовуйте AsyncPipeякнайбільше, оскільки він автоматично скасовує підписку на спостереження за знищенням компонентів.
  • Скасуйте підписку на ActivatedRouteспостережувані дані, наприклад, route.paramsякщо вони підписані всередині вкладеного (Додано всередині tpl із селектором компонентів) або динамічного компонента, оскільки вони можуть бути підписані багато разів, поки існує батьківський / хост-компонент. Не потрібно скасовувати підписку на інші сценарії, як зазначено у цитаті вище в Routing & Navigationдокументах.
  • Скасуйте підписку на глобальні спостереження, які поділяються між компонентами, які експонуються через службу Angular, наприклад, оскільки вони можуть бути підписані кілька разів, поки компонент ініціалізований.
  • Немає необхідності скасовувати підписку на внутрішні спостереження служби, що охоплює програму, оскільки ця послуга ніколи не знищується, якщо вся ваша програма не буде знищена, немає жодної реальної причини відмовитися від неї і немає шансів на витік пам'яті. (6) .

    Примітка. Що стосується сфери послуг, тобто постачальників компонентів, вони знищуються, коли компонент знищений. У цьому випадку, якщо ми підписуємося на будь-який спостерігається всередині цього провайдера, нам слід розглянути можливість підписки на нього за допомогою OnDestroyгака життєвого циклу, який буде викликаний при знищенні служби, згідно з документами.
  • Використовуйте абстрактну техніку, щоб уникнути будь-якого безладу коду, який може бути наслідком скасування підписки. Ви можете керувати своїми підписками за допомогою takeUntil (3) або використовувати цей npm пакет, згаданий у (4). Найпростіший спосіб скасувати підписку на Observables in Angular .
  • Завжди скасовуйте підписку на FormGroupспостереження, як-от form.valueChangesіform.statusChanges
  • Завжди відписуйтесь від спостережуваних Renderer2служб, як-отrenderer2.listen
  • Скасуйте підписку на кожне спостережуване інше як крок захисту від витоку пам'яті, поки Angular Docs чітко не скаже нам, які спостережні дані не потрібно підписувати (Перевірте випуск: (5) Документація на скасування підписки на RxJS (Відкрито) ).
  • Бонус: Завжди використовуйте кутові способи прив’язки подій, як- HostListenerот кутовий, добре піклується про видалення слухачів подій, якщо це потрібно, і запобігає можливому витоку пам’яті через прив’язку подій.

Приємна остаточна порада : Якщо ви не знаєте, чи спостерігається автоматично відписаний / завершений чи ні, додайте completeзворотний дзвінок subscribe(...)і перевірте, чи отримує виклик його, коли компонент знищений.


Відповідь №6 не зовсім правильна. Служби знищуються і їх ngOnDestroyвикликають, коли послуга надається на рівні, відмінному від рівня кореня, наприклад, надається явно в компоненті, який пізніше видаляється. У цих випадках слід скасувати підписку на внутрішні спостереження
Drenai

@Drenai, дякую за ваш коментар і ввічливо я не згоден. Якщо компонент знищений, компонент, сервіс і спостережуваний буде всім GCed, і підписка буде марною в цьому випадку, якщо ви не будете зберігати посилання на предмет, що спостерігається десь від компонента (Що не логічно просочувати стан компонентів у всьому світі не дивлячись на те, щоб скористатися послугою до компонента)
Mouneer

Якщо служба, яка знищується, має передплату на спостережувану, що належить іншій службі вище в ієрархії DI, GC не відбудеться. Уникайте цього сценарію, скасувавши підписку ngOnDestroy, яка завжди викликається, коли сервіси знищені github.com/angular/angular/commit/…
Drenai

1
@Drenai, Перевір оновлену відповідь.
Муньє

2
@Tim Перш за все, Feel free to unsubscribe anyway. It is harmless and never a bad practice.і щодо вашого питання це залежить. Якщо дочірній компонент ініціюється кілька разів (наприклад, додається всередину ngIfабо завантажується динамічно), ви повинні скасувати підписку, щоб уникнути додавання декількох підписок до одного і того ж спостерігача. Інакше не потрібно. Але я вважаю за краще скасовувати підписку всередині дочірнього компонента, оскільки це робить його більш багаторазовим та ізольованим від способу його використання.
Mouneer

18

Це залежить. Якщо зателефонувавши someObservable.subscribe(), ви почнете затримувати деякий ресурс, який потрібно звільнити вручну, коли закінчиться життєвий цикл вашого компонента, тоді вам слід зателефонувати, theSubscription.unsubscribe()щоб запобігти витоку пам'яті.

Давайте докладніше розглянемо ваші приклади:

getHero()повертає результат http.get(). Якщо ви подивитесь на кутовий 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()підлягає вивезенню сміття, як тільки це буде зроблено, навіть якщо до нього приєднаються слухачі подій. Тож я думаю, що тому офіційний посібник у кутовому режимі 2 опускає unsubscribe()та дозволяє GC прибирати слухачів.

Що стосується вашого другого прикладу, це залежить від реалізації params. На сьогоднішній день у кутовому офіційному довіднику більше не відображається підписка на params. Я знову заглянув у src і виявив, що paramsце просто Поведінка . Оскільки слухачів або таймерів подій не використовувались і глобальних змінних не було створено, це слід безпечно опустити unsubscribe().

Підсумком вашого запитання є те, що завжди дзвоніть unsubscribe()як захист від витоку пам’яті, якщо ви не впевнені, що виконання спостережуваного не створює глобальних змінних, додайте слухачів подій, встановлюйте таймери чи щось інше, що призводить до витоку пам'яті .

Коли ви сумніваєтесь, погляньте на реалізацію цього спостережуваного. Якщо спостережуваний записав свою логіку очищення unsubscribe(), яка зазвичай є функцією, яку повертає конструктор, то у вас є вагомі підстави серйозно задуматися про виклик unsubscribe().


6

Офіційна документація Angular 2 надає пояснення, коли потрібно скасувати підписку та коли її можна сміливо ігнорувати. Подивіться на це посилання:

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

Шукайте абзац із заголовком Батько та діти спілкуються через службу, а потім синє поле:

Зауважте, що ми фіксуємо підписку і скасовуємо підписку, коли AstronautComponent знищений. Це крок охоронної пам'яті. Фактичного ризику в цьому додатку немає, оскільки термін експлуатації AstronautComponent такий же, як і термін служби самого додатка. Це не завжди було б правдою в більш складному застосуванні.

Ми не додаємо цей захист до MissionControlComponent, тому що він, як батько, контролює термін служби MissionService.

Я сподіваюся, що це вам допоможе.


3
як компонент ви ніколи не знаєте, дитина ви чи ні. тому завжди слід скасувати підписку на найкращу практику.
SeriousM

1
Справа про MissionControlComponent не в тому, щоб це був батько чи ні, це те, що компонент сам надає послугу. Коли MissionControl знищується, це також робить сервіс та будь-які посилання на екземпляр служби, тому немає можливості витоку.
закінчується

6

На основі: Використання успадкування класів для підключення до 2-х компонентних життєвих циклів Angular

Ще один загальний підхід:

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));
    }
}


1
Це НЕ працює належним чином. Будьте обережні, використовуючи це рішення. Вам не вистачає this.componentDestroyed$.next()дзвінка, як прийняте рішення від Sean вище ...
philn

4

Офіційна відповідь редагування №3 (і варіанти) працює добре, але те, що мене отримує, - це "затуманення" ділової логіки навколо спостережуваної підписки.

Ось ще один підхід із використанням обгортки.

Попередження: експериментальний код

Файл SubscriAndGuard.ts використовується для створення нового розширеного спостережуваного розширення для завершення, .subscribe()а в ньому - для завершення ngOnDestroy().
Використання таке ж .subscribe(), за винятком додаткового першого параметра, на який посилається компонент.

import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';

const subscribeAndGuard = function(component, fnData, fnError = null, fnComplete = null) {

  // Define the subscription
  const sub: Subscription = this.subscribe(fnData, fnError, fnComplete);

  // Wrap component's onDestroy
  if (!component.ngOnDestroy) {
    throw new Error('To use subscribeAndGuard, the component must implement ngOnDestroy');
  }
  const saved_OnDestroy = component.ngOnDestroy;
  component.ngOnDestroy = () => {
    console.log('subscribeAndGuard.onDestroy');
    sub.unsubscribe();
    // Note: need to put original back in place
    // otherwise 'this' is undefined in component.ngOnDestroy
    component.ngOnDestroy = saved_OnDestroy;
    component.ngOnDestroy();

  };

  return sub;
};

// Create an Observable extension
Observable.prototype.subscribeAndGuard = subscribeAndGuard;

// Ref: https://www.typescriptlang.org/docs/handbook/declaration-merging.html
declare module 'rxjs/Observable' {
  interface Observable<T> {
    subscribeAndGuard: typeof subscribeAndGuard;
  }
}

Ось компонент з двома підписками, один із обгорткою та один без. Єдиний застереження: він повинен реалізовувати OnDestroy (при бажанні з порожнім корпусом), інакше Angular не знає називати загорнуту версію.

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

@Component({
  selector: 'app-subscribing',
  template: '<h3>Subscribing component is active</h3>',
})
export class SubscribingComponent implements OnInit, OnDestroy {

  ngOnInit() {

    // This subscription will be terminated after onDestroy
    Observable.interval(1000)
      .subscribeAndGuard(this,
        (data) => { console.log('Guarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );

    // This subscription will continue after onDestroy
    Observable.interval(1000)
      .subscribe(
        (data) => { console.log('Unguarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );
  }

  ngOnDestroy() {
    console.log('SubscribingComponent.OnDestroy');
  }
}

Демонстраційний plunker знаходиться тут

Додаткова примітка: Re Edit 3 - "Офіційне" рішення, це можна спростити за допомогою використання takeWhile () замість takeUntil () перед підписками та простого булевого, а не іншого, що спостерігається в ngOnDestroy.

@Component({...})
export class SubscribingComponent implements OnInit, OnDestroy {

  iAmAlive = true;
  ngOnInit() {

    Observable.interval(1000)
      .takeWhile(() => { return this.iAmAlive; })
      .subscribe((data) => { console.log(data); });
  }

  ngOnDestroy() {
    this.iAmAlive = false;
  }
}

3

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

Ця відповідь дає спосіб звільнитись від супервиклику та зробити "компонентDestroyed $" основним базовим компонентом.

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');
    }

}

3

Після відповіді @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()якщо ви також реалізуєте цей метод у дочірньому компоненті.

Знайдіть тут додаткову довідку Бена Леша: RxJS: Не скасовуйте підписку .


2

Я спробував рішення Seangwright (Правка 3)

Це не працює для спостерігається, що створено таймером або інтервалом.

Однак у мене це працює, використовуючи інший підхід:

import { Component, OnDestroy, OnInit } from '@angular/core';
import 'rxjs/add/operator/takeUntil';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/Rx';

import { MyThingService } from '../my-thing.service';

@Component({
   selector: 'my-thing',
   templateUrl: './my-thing.component.html'
})
export class MyThingComponent implements OnDestroy, OnInit {
   private subscriptions: Array<Subscription> = [];

  constructor(
     private myThingService: MyThingService,
   ) { }

  ngOnInit() {
    const newSubs = this.myThingService.getThings()
        .subscribe(things => console.log(things));
    this.subscriptions.push(newSubs);
  }

  ngOnDestroy() {
    for (const subs of this.subscriptions) {
      subs.unsubscribe();
   }
 }
}

2

Мені подобаються останні два відповіді, але у мене виникло питання, якщо підклас посилався "this"наngOnDestroy .

Я змінив це так, і, схоже, вирішив це питання.

export abstract class BaseComponent implements OnDestroy {
    protected componentDestroyed$: Subject<boolean>;
    constructor() {
        this.componentDestroyed$ = new Subject<boolean>();
        let f = this.ngOnDestroy;
        this.ngOnDestroy = function()  {
            // without this I was getting an error if the subclass had
            // this.blah() in ngOnDestroy
            f.bind(this)();
            this.componentDestroyed$.next(true);
            this.componentDestroyed$.complete();
        };
    }
    /// placeholder of ngOnDestroy. no need to do super() call of extended class.
    ngOnDestroy() {}
}

вам потрібно скористатися функцією стрілки, щоб прив’язати "це":this.ngOnDestroy = () => { f.bind(this)(); this.componentDestroyed$.complete(); };
Дамсоріан

2

Якщо потрібна скасування підписки, може бути використаний наступний оператор для способу спостережуваної труби

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 методом.

Важливо: оператор повинен бути останнім у трубі, що спостерігається.


Це спрацювало чудово, однак оновлення до кутового 9, здається, вбиває його. Хтось знає, чому?
ymerej

1

Зазвичай вам потрібно скасувати підписку, коли компоненти будуть знищені, але Angular буде працювати з цим все більше і більше, як ми йдемо, наприклад, у новій незначній версії Angular4, у них є цей розділ для маршрутизації, щоб скасувати підписку:

Вам потрібно скасувати підписку?

Як описано в ActivateRoute: в розділі "Інформація про маршрут" на сторінці маршрутизації та навігації "єдиний пристрій" маршрутизатор управляє спостереженнями, які він надає, та локалізує підписки. Підписки очищаються, коли компонент знищений, захищаючи від витоків пам’яті, тому не потрібно скасовувати підписку на парам-мапу маршруту.

Також наведений нижче приклад - хороший приклад від 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();
  }
}

3
Плутати. Що ти тут кажеш? Ви (нещодавні документи / нотатки Angular), здається, говорите, що Angular піклується про це, а потім пізніше підтверджуєте, що скасування підписки є хорошим зразком. Дякую.
jamie

1

Ще одне коротке доповнення до вищезазначених ситуацій:

  • Завжди скасовуйте підписку, коли нові значення в підписаному потоці більше не потрібні або не мають значення, це призведе до меншої кількості тригерів і збільшення продуктивності в кількох випадках. Такі випадки, як компоненти, у яких більше не існує передплачених даних / події або потрібна нова підписка на весь новий потік (оновлення тощо) - хороший приклад для скасування передплати.

0

в додатку SPA за допомогою функції ngOnDestroy (angular lifeCycle). Для кожної підписки потрібно скасувати підписку . перевага =>, щоб запобігти занадто важкому стану.

наприклад: у компоненті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();
}

0

Для обробки підписки я використовую клас "Відписатись".

Ось клас скасування підписки.

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);
}

0

Ви можете скористатися останнім Subscriptionкласом, щоб скасувати підписку на спостерігається з не надто безладним кодом.

Ми можемо це зробити, normal variableале це буде override the last subscriptionпри кожній новій підписці, тому уникайте цього, і цей підхід дуже корисний, коли ви маєте справу з більшою кількістю обсервабелів та типом спостережуваних, як BehavoiurSubjectіSubject

Підписка

Представляє одноразовий ресурс, такий як виконання спостережуваного. У підписки є один важливий метод - скасувати підписку, який не бере аргументів і просто розпоряджається ресурсом, який зберігається підпискою.

ви можете використовувати це двома способами,

  • ви можете безпосередньо підштовхнути підписку до масиву підписок

     subscriptions:Subscription[] = [];
    
     ngOnInit(): void {
    
       this.subscription.push(this.dataService.getMessageTracker().subscribe((param: any) => {
                //...  
       }));
    
       this.subscription.push(this.dataService.getFileTracker().subscribe((param: any) => {
            //...
        }));
     }
    
     ngOnDestroy(){
        // prevent memory leak when component destroyed
        this.subscriptions.forEach(s => s.unsubscribe());
      }
    
  • використовуючи add()вSubscription

    subscriptions = new Subscription();
    
    this.subscriptions.add(subscribeOne);
    this.subscriptions.add(subscribeTwo);
    
    ngOnDestroy() {
      this.subscriptions.unsubscribe();
    }
    

А Subscriptionможе зберігати підписки на дітей і безпечно скасовувати їх передплату. Цей метод обробляє можливі помилки (наприклад, якщо будь-які дочірні підписки є недійсними).

Сподіваюся, це допомагає .. :)


0

Пакет SubSink - це просте та послідовне рішення для скасування передплати

Оскільки ніхто інший не згадував про це, я хочу рекомендувати пакет для підписки, створений Уордом Беллом: https://github.com/wardbell/subsink#readme .

Я використовую його в проекті, якщо ми кілька розробників, які все це використовують. Дуже допомагає мати послідовний спосіб, який працює у будь-якій ситуації.


0

Для спостережних даних, які завершуються безпосередньо після видачі результату, наприклад, AsyncSubjectабо, наприклад, спостереження з http-запитів, і такі не потрібно скасовувати підписку. Не завадить зателефонувати unsubscribe()тим, але якщо спостерігається closedметод відписки , просто нічого не зробить :

if (this.closed) {
  return;
}

Якщо у вас є довговічні спостереження, які випромінюють кілька значень протягом часу (наприклад, a BehaviorSubjectабо a ReplaySubject), вам потрібно скасувати підписку, щоб запобігти витоку пам'яті.

Ви можете легко створити спостережуване, що завершується безпосередньо після випромінювання результату від таких довговічних спостережень за допомогою трубного оператора. У деяких відповідях тут take(1)згадується труба. Але я вважаю за краще в first()трубу . Різниця в take(1)тому, що це:

доставити EmptyErrorзворотній виклик помилки Спостерігача, якщо спостережуване завершиться до надсилання будь-якого наступного повідомлення.

Ще одна перевага першої труби полягає в тому, що ви можете передавати предикат, який допоможе повернути перше значення, яке відповідає певним критеріям:

const predicate = (result: any) => { 
  // check value and return true if it is the result that satisfies your needs
  return true;
}
observable.pipe(first(predicate)).subscribe(observer);

Спочатку буде завершено безпосередньо після випромінювання першого значення (або при передачі аргументу функції перше значення, яке задовольняє ваш присудок), тому немає необхідності скасовувати підписку.

Іноді ти не впевнений у тому, довго спостерігаєш чи ні. Я не кажу, що це є хорошою практикою, але ви завжди можете додавати firstтрубу просто для того, щоб переконатися, що вам не потрібно буде вручну скасовувати підписку. Додавання додаткової firstтруби до спостережуваного, що випромінює лише одне значення, не завадить.

В процесі розробки ви можете використовувати в singleтрубу , яка буде виконана , якщо вихідні спостерігаються випускає кілька подій. Це може допомогти вам вивчити тип спостережуваного та чи потрібно скасовувати підписку на нього чи ні.

observable.pipe(single()).subscribe(observer);

firstІ singleздається дуже схожі, обидві труба може взяти додатковий предикат , але відмінності є важливими і добре представлені в цьому StackOverflow відповіді тут :

Спочатку

Показуватиметься, як тільки з’явиться перший елемент. Завершиться одразу після цього.

Неодружений

Вийде з ладу, якщо спостережуване джерело випромінює кілька подій.


Примітка. У своїй відповіді я намагався бути максимально точним та повноцінним із посиланням на офіційну документацію, але, будь ласка, прокоментуйте, якщо чогось важливого немає?


-1

--- Оновлення рішення Angular 9 та Rxjs 6

  1. Використання unsubscribeв ngDestroyжиттєвому циклі кутового компонента
class SampleComponent implements OnInit, OnDestroy {
  private subscriptions: Subscription;
  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$.subscribe( ... );
  }

  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}
  1. Використання takeUntilв Rxjs
class SampleComponent implements OnInit, OnDestroy {
  private unsubscribe$: new Subject<void>;
  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$
    .pipe(takeUntil(this.unsubscribe$))
    .subscribe( ... );
  }

  ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}
  1. для деяких дій, які ви викликаєте в ngOnInitцей момент, відбувається лише один раз, коли компонент init.
class SampleComponent implements OnInit {

  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$
    .pipe(take(1))
    .subscribe( ... );
  }
}

У нас також є asyncтруба. Але це використання в шаблоні (не в кутовій складовій).


Ваш перший приклад неповний.
Пол-Себастьян Маноле
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.