Angular / RxJs Когда я должен отписаться от `Подписки`

720

Когда я должен хранить Subscriptionэкземпляры и вызывать их 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();
}
Сергей Тихон
источник
21
Я думаю, Subscriptionчто http-requestsможно игнорировать, так как они звонят только onNextодин раз, а затем они звонят onComplete. RouterВместо вызовов onNextнесколько раз и никогда не может назвать onComplete(не уверен , что ...). То же самое касается Observables от Events. Так что, думаю, так и должно быть unsubscribed.
Спрингрбуа
1
@ gt6707a Поток завершается (или не завершается) независимо от наблюдения этого завершения. Обратные вызовы (наблюдатель), предоставляемые функции подписки, не определяют, выделены ли ресурсы. Это вызов самому subscribeсебе, который потенциально выделяет ресурсы в восходящем направлении.
seangwright

Ответы:

981

--- Редактировать 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://ncjamieson.com/avoiding-takeuntil-leaks/

  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 заметить Observable из компонента Feature в компонент InputPresentational, что означает, что подписки нигде не нужны. Подробнее об этом подходе читайте здесь

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

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

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

Затем мы позвоним 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 теперь заявляет следующее: «Маршрутизатор управляет наблюдаемыми объектами, которые он предоставляет, и локализует подписки. Подписки очищаются при уничтожении компонента, защищая от утечек памяти, поэтому нам не нужно отписываться от параметры маршрута наблюдаемые. " - Марк Райкок

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

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

Источник 4

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

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

TLDR:

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

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

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

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

Источник 1

Я разыскал ответ Роба Вормальда в Angular's Gitter здесь .

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

если это однозначная последовательность (например, 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 ?

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

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

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

Источник 3

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

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

seangwright
источник
16
Вызов complete()сам по себе не может очистить подписки. Как бы то ни было, вызов, next()а затем complete()и выполнение, я полагаю, takeUntil()прекращается только при получении значения, а не при завершении последовательности.
Firefly
3
@seangwright Быстрый тест с элементом типа Subjectвнутри компонента и переключением его с помощью ngIfтриггера ngOnInitи ngOnDestroyпоказывает, что субъект и его подписки никогда не завершатся или не будут уничтожены (подключен оператор finally-подписчик к подписке). Я должен позвонить Subject.complete()в ngOnDestroy, так подписки можно убирать за собой.
Ларс
4
Ваш --- Edit 3 очень проницательный, спасибо! У меня просто есть дополнительный вопрос: если использовать takeUnitlподход, нам никогда не придется вручную отписываться от каких-либо наблюдаемых? Это тот случай? Кроме того, почему мы должны вызвать next()в ngOnDestroy, почему бы просто не позвонить complete()?
uglycode
7
@seangwright Это разочаровывает; дополнительный шаблон раздражает.
spongessuck
4
Редактирование 3 обсуждается в контексте событий в medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87
HankCa
91

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

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 . Вы можете предпочесть это, но имейте в виду, что таким образом выполнение Observable не будет отменено для 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
  }
}
metamaker
источник
2
Если он просто использует bool для сохранения состояния, как заставить "takeUntil" работать как положено?
Val
6
Я думаю, что есть существенная разница между использованием takeUntilи takeWhile. Первый отписывается от источника, наблюдаемого сразу после запуска, в то время как второй отписывается только после того, как источник получает наблюдаемое значение. Если создание значения с помощью источника, наблюдаемого является операцией, потребляющей ресурсы, выбор между ними может выходить за рамки предпочтений стиля. Увидеть планк
Алекс Че
2
@AlexChe спасибо за интересный план! Это очень важный момент для общего использования takeUntilпротив takeWhile, но не для нашего конкретного случая. Когда нам нужно отписаться от подписчиков на уничтожение компонентов , мы просто проверяем логическое значение, как () => aliveв takeWhile, так что любые операции, занимающие время / память, не используются, и различие в значительной степени связано со стилем (ofc, для этого конкретного случая).
метамейкер
2
@metamaker Скажем, в нашем компоненте мы подписываемся на an Observable, которая внутренне добывает некоторую криптовалюту и запускает nextсобытие для каждой добытой монеты, а добыча одной такой монеты занимает день. С помощью takeUntilмы отписываемся от источника майнинга Observableсразу, как только ngOnDestroyвызывается во время уничтожения нашего компонента. Таким образом, Observableфункция майнинга может немедленно отменить свою работу во время этого процесса.
Алекс Че
2
OTOH, если мы используем takeWhile, в ngOnDestoryмы просто устанавливаем логическую переменную. Но Observableфункция майнинга может работать до одного дня, и только тогда во время nextвызова она поймет, что активных подписок нет, и ее нужно отменить.
Алекс Че
76

Класс подписки имеет интересную особенность:

Представляет одноразовый ресурс, такой как выполнение Observable. Подписка имеет один важный метод, отписаться, который не принимает аргументов и просто удаляет ресурс, удерживаемый подпиской.
Кроме того, подписки могут быть сгруппированы с помощью метода 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();
  }
}
Стивен Лиекенс
источник
1
Я использую этот подход. Хотите знать, если это лучше, чем использовать подход с takeUntil (), как в принятом ответе .. недостатки?
Мануэль Ди
1
Никаких недостатков, о которых я знаю. Я не думаю, что это лучше, просто по-другому.
Стивен Лиекенс
3
См. Medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 для дальнейшего обсуждения официального takeUntilподхода по сравнению с этим подходом сбора подписок и вызовов unsubscribe. (Этот подход кажется мне чище.)
Джош Келли
4
Одно небольшое преимущество этого ответа: вам не нужно проверять, имеет ли значение this.subscriptionsnull
user2023861
2
Просто избегайте цепочки методов add, например, sub = subsciption.add(..).add(..)потому что во многих случаях это приводит к неожиданным результатам github.com/ReactiveX/rxjs/issues/2769#issuecomment-345636477
Евгений Генералов
32

Некоторые из лучших практик относительно наблюдаемых отписок внутри угловых компонентов:

Цитата из Routing & Navigation

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

Есть несколько исключительных наблюдаемых, где это не обязательно. Наблюдаемые значения ActivatedRoute входят в число исключений.

ActivatedRoute и его наблюдаемые изолированы от самого маршрутизатора. Маршрутизатор уничтожает маршрутизируемый компонент, когда он больше не нужен, и внедренный ActivatedRoute умирает вместе с ним.

Не стесняйтесь отписаться в любом случае. Это безвредно и никогда не бывает плохой практикой.

И в ответ на следующие ссылки:

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

  • httpнаблюдаемая отмена подписки является условной, и мы должны учитывать последствия «обратного вызова подписки», запускаемого после уничтожения компонента в каждом конкретном случае. Мы знаем, что угол отписывается и очищает httpсаму наблюдаемую (1) , (2) . Хотя это верно с точки зрения ресурсов, это только половина истории. Допустим, мы говорим о прямом звонкеhttp из компонента, аhttp ответ занял больше времени, чем нужно, поэтому пользователь закрыл компонент. subscribe()Обработчик будет по-прежнему вызываться, даже если компонент закрыт и уничтожен. Это может иметь нежелательные побочные эффекты, а в худших сценариях состояние приложения нарушается. Это также может вызвать исключения, если код в обратном вызове пытается вызвать что-то, что только что было удалено. Однако в то же время иногда они желательны. Например, предположим, что вы создаете почтовый клиент и запускаете звуковой сигнал, когда электронная почта завершает отправку - хорошо, что вы все равно хотите, чтобы это произошло, даже если компонент закрыт ( 8 ).
  • Не нужно отписываться от наблюдаемых или завершенных ошибок. Тем не менее, в этом нет никакого вреда (7) .
  • Используйте AsyncPipeкак можно больше, потому что он автоматически отписывается от наблюдаемого при уничтожении компонента.
  • Отписаться от ActivatedRouteнаблюдаемых, например, route.paramsесли они подписаны внутри вложенного (добавлено в tpl с помощью селектора компонента) или динамического компонента, так как они могут быть подписаны много раз, пока существует родительский / хост-компонент. Нет необходимости отписываться от них в других сценариях, как указано в приведенной выше цитате изRouting & Navigation документации.
  • Отписаться от глобальных наблюдаемых, общих для компонентов, которые предоставляются через службу Angular, например, так как они могут быть подписаны несколько раз, пока компонент инициализирован.
  • Не нужно отписываться от внутренних наблюдаемых службы в области приложения, так как эта служба никогда не будет уничтожена, если только все ваше приложение не будет уничтожено, нет реальной причины отказаться от нее и нет шансов утечки памяти. (6) .

    Примечание. Что касается сервисов с определенной областью, то есть поставщиков компонентов, они уничтожаются при уничтожении компонента. В этом случае, если мы подписываемся на какой-либо наблюдаемый внутри этого провайдера, мы должны рассмотреть возможность отказа от него, используяOnDestroy ловушку жизненного цикла, которая будет вызываться при разрушении службы, согласно документам.
  • Используйте абстрактную технику, чтобы избежать путаницы в коде, которая может возникнуть в результате отписки. Вы можете управлять своими подписками с помощью takeUntil (3) или использовать этот npm пакет, упомянутый в (4) Самый простой способ отписаться от Observables в Angular .
  • Всегда отписываться от FormGroupнаблюдаемых как form.valueChangesиform.statusChanges
  • Всегда отписываться от наблюдаемой Renderer2службы, какrenderer2.listen
  • Отмените подписку от всех других наблюдаемых в качестве шага защиты от утечки памяти, пока Angular Docs явно не скажет нам, какие наблюдаемые необязательны для отмены подписки (Проверьте проблему: (5) Документация для RxJS Unsubscribeing (Open) ).
  • Бонус: всегда используйте угловые способы привязки событий, например, так HostListenerкак angular заботится об удалении слушателей событий, если это необходимо, и предотвращает любую потенциальную утечку памяти из-за привязок событий.

Хороший заключительный совет : если вы не знаете, автоматически ли отписывается / завершается заметка или нет, добавьте completeобратный вызов subscribe(...)и проверьте, вызывается ли он при уничтожении компонента.

Mouneer
источник
Ответ за № 6 не совсем правильный. Службы уничтожаются, и их ngOnDestroyвызывают, когда служба предоставляется на уровне, отличном от корневого, например, предоставляется явно в компоненте, который впоследствии удаляется. В этих случаях вам следует отписаться от сервисов внутренних
заметок
@Drenai, спасибо за ваш комментарий и вежливо я не согласен. Если компонент уничтожен, компонент, служба и наблюдаемое все будут GCed, и отмена подписки будет бесполезна в этом случае, если вы не сохраните ссылку на наблюдаемое где-либо далеко от компонента (что не логично для утечки состояний компонента глобально несмотря на определение
объема
Если уничтожаемая служба имеет подписку на наблюдаемую принадлежность другой службы, находящейся выше в иерархии DI, GC не будет выполняться. Избегайте этого сценария, отменив подписку ngOnDestroy, которая всегда вызывается при уничтожении сервисов github.com/angular/angular/commit/…
Drenai
1
@Drenai, проверь обновленный ответ.
Mouneer
3
@Tim Прежде всего, Feel free to unsubscribe anyway. It is harmless and never a bad practice.и что касается вашего вопроса, это зависит. Если дочерний компонент запускается несколько раз (например, добавляется внутри ngIfили загружается динамически), необходимо отменить подписку, чтобы избежать добавления нескольких подписок к одному и тому же наблюдателю. В противном случае нет необходимости. Но я предпочитаю отписываться внутри дочернего компонента, так как это делает его более пригодным для повторного использования и изолированным от того, как его можно использовать.
Mouneer
18

Это зависит. Если при вызове 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().

Chuanqi Sun
источник
6

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

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

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

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

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

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

Cerny
источник
3
как компонент, вы никогда не знаете, являетесь ли вы ребенком или нет. поэтому вы всегда должны отписываться от подписки как лучшая практика.
SeriousM
1
Дело в том, что MissionControlComponent на самом деле не в том, является ли он родительским или нет, а в том, что компонент сам предоставляет сервис. Когда уничтожается MissionControl, то же самое происходит и с сервисом, и с любыми ссылками на экземпляр сервиса, поэтому нет возможности утечки.
конец
6

Основано на: Использование наследования классов для привязки к жизненному циклу компонента 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));
    }
}

толчок
источник
1
Это НЕ работает правильно. Пожалуйста, будьте осторожны при использовании этого решения. Вы пропускаете this.componentDestroyed$.next()вызов , как общепринятое решение Шона выше ...
philn
4

Официальный ответ Edit # 3 (и варианты) работает хорошо, но меня привлекает «мутность» бизнес-логики вокруг наблюдаемой подписки.

Вот еще один подход с использованием оберток.

Warining: экспериментальный код

Файл subscribeAndGuard.ts используется для создания нового расширения Observable для переноса .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');
  }
}

Демоверсия здесь

Дополнительное примечание: Re Edit 3 - «Официальное» решение, это можно упростить, используя takeWhile () вместо takeUntil () перед подпиской, и просто логическое значение, а не другое Observable в 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, чтобы активировать эту функцию.

Этот ответ предоставляет способ освободиться от супер-вызова и сделать «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');
    }

}
Val
источник
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 (Edit 3)

Это не работает для Observable, созданного таймером или интервалом.

Тем не менее, я получил это с помощью другого подхода:

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':this.ngOnDestroy = () => { f.bind(this)(); this.componentDestroyed$.complete(); };
Damsorian
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.

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

Олег Полезский
источник
Это сработало замечательно, однако обновление до angular 9, похоже, убило его. Кто-нибудь знает почему?
ymerej
1

Обычно вам нужно отписаться, когда компоненты будут уничтожены, но 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();
  }
}
Алиреза
источник
3
Смущенный. Что ты здесь говоришь? Вы (Angular недавние документы / заметки), кажется, говорите, что Angular позаботится об этом, а затем позже, чтобы подтвердить, что отмена подписки - это хороший шаблон. Спасибо.
Jamie
1

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

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

в применении 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();
}
мойтаба рамезани
источник
0

Для обработки подписки я использую класс «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);
}
Pratiyush
источник
0

Вы можете использовать последний Subscriptionкласс, чтобы отказаться от подписки на Observable с не таким грязным кодом.

Мы можем сделать это, normal variableно это будет override the last subscriptionпри каждой новой подписке, поэтому избегайте этого, и этот подход очень полезен, когда вы имеете дело с большим количеством Obseravables и типа Obeservables, таких как BehavoiurSubjectиSubject

Подписка

Представляет одноразовый ресурс, такой как выполнение Observable. Подписка имеет один важный метод, отписаться, который не принимает аргументов и просто удаляет ресурс, удерживаемый подпиской.

Вы можете использовать это двумя способами,

  • Вы можете напрямую передать подписку на массив подписок

     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может проводить дочерние подписки и безопасно отписываться от них всех. Этот метод обрабатывает возможные ошибки (например, если какие-либо дочерние подписки нулевые).

Надеюсь это поможет.. :)

ganesh045
источник
0

Пакет SubSink, простое и последовательное решение для отказа от подписки

Поскольку никто другой не упомянул об этом, я хочу порекомендовать пакет Subsink, созданный Ward Bell: https://github.com/wardbell/subsink#readme .

Я использовал его в проекте, где мы работали с несколькими разработчиками. Очень помогает иметь последовательный способ, который работает в любой ситуации.

SnorreDan
источник
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 Solution

  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труба. Но это использование в шаблоне (не в угловом компоненте).

Хоанг Тран Сын
источник
Ваш первый пример неполон.
Пол-Себастьян Маноле