RxJS: Не отписывайся / Хабр

RxJS: Не отписывайся / Хабр Подписки

Прекратите выдавать за утечку памяти что-то другое

На сегодняшний день написано оченьмногостатей о том, что от подписок Observable RxJS надо отписываться, иначе произойдет утечка памяти. У большинства читателей таких статей в голове отложилось твёрдое правило “подписался? — отпишись!”. Но, к сожалению, зачастую в подобных статьях информация искажается или что-то недоговаривается, а ещё хуже когда подменяются понятия. Об этом и поговорим.

RxJS: Не отписывайся / Хабр

Возьмем, к примеру, эту статью: https://medium.com/ngx/why-do-you-need-unsubscribe-ee0c62b5d21f

RxJS: Не отписывайся / Хабр

Когда мне говорят про “потенциальную возможность получить регрессию в производительности” я сразу вспоминаю про преждевременную оптимизацию.

RxJS: Не отписывайся / Хабр

RxJS: Не отписывайся / Хабр

Продолжим читать статью человека под ником Reactive Fox:
RxJS: Не отписывайся / Хабр

RxJS: Не отписывайся / Хабр

Дальше есть полезная информация и советы. Я согласен, что нужно всегда отписываться от бесконечных стримов в RxJS. Но я акцентирую внимание только на вредной информации (по моему мнению).
RxJS: Не отписывайся / Хабр

Ух… нагнал жути. Такие бездоказательные (нет метрик, цифр…) запугивания в настоящее время привели к тому, что для очень большого числа фронтендеров отсутствие отписки это как красная тряпка для быка. Когда они на это натыкаются, они больше не видят ничего вокруг, кроме этой тряпки.
RxJS: Не отписывайся / Хабр

Автор статьи даже сделал демо-приложение, где попытался доказать свои размышления:
https://stackblitz.com/edit/why-you-have-to-unsubscribe-from-observable-material

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

RxJS: Не отписывайся / Хабр

Как подтверждение того, что нужно всегда отписываться от подписок Observable запросов HttpClient, он добавил такой перехватчик запросов, который нам выводит в консоль “still alive… still alive… still alive…”:
RxJS: Не отписывайся / Хабр
Т.е. человек перехватил конечный стрим, сделал из него бесконечный (в случае ошибки происходит повтор запроса, а ошибка происходит всегда) и выдает это за доказательство того, что нужно отписываться от конечных.

StackBlitz не очень подходит для измерения производительности приложений, т.к. там есть автоматическая синхронизация при обновлении и это отнимает ресурсы. Поэтому я сделал своё тестовое приложение: https://github.com/andchir/test-angular-app

Там есть два окошка. При открытии каждого отправляется запрос на action.php, в котором есть задержка в 3 секунды как имитация выполнения очень ресурсоёмкой операции. Также action.php логирует все запросы в файл log.txt.

Но сначала небольшое отступление. На картинке ниже (кликабельно) вы можете увидеть простой пример как работает сборщик мусора JavaScript в браузере Chrome. PUSH произошел, но setTimeout не помешал сборщику мусора очистить память.
RxJS: Не отписывайся / Хабр

Не забывайте вызывать сборщик мусора нажатием кнопки, когда будете экспериментировать.
RxJS: Не отписывайся / Хабр

Вернемся к моему тестовому приложению. Вот код обоих окошек:

Код BadModalComponent
@Component({
    selector: 'app-bad-modal',
    templateUrl: './bad-modal.component.html',
    styleUrls: ['./bad-modal.component.css'],
    providers: [HttpClient]
})
export class BadModalComponent implements OnInit, OnDestroy {

    loading = false;
    largeData: number[] = (new Array(1000000)).fill(1);
    destroyed$ = new Subject<void>();
    data: DataInterface;

    constructor(
        private http: HttpClient,
        private bsModalRef: BsModalRef
    ) {
    }

    ngOnInit() {
        this.loadData();
    }

    loadData(): void {
        // For example only, not for production.

        this.loading = true;
        const subscription = this.http.get<DataInterface>('/action.php?a=2').pipe(
                takeUntil(this.destroyed$),
                catchError((err) => throwError(err.message)),
                finalize(() => console.log('FINALIZE'))
            )
            .subscribe({
                next: (res) => {
                    setTimeout(() => {
                        console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!');
                    }, 0);
                    console.log('LOADED');
                    this.data = res;
                    this.loading = false;
                },
                error: (error) => {
                    setTimeout(() => {
                        console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!');
                    }, 0);
                    console.log('ERROR', error);
                },
                complete: () => {
                    setTimeout(() => {
                        console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!');
                    }, 0);
                    console.log('COMPLETED');
                }
            });
    }

    close(event?: MouseEvent): void {
        if (event) {
            event.preventDefault();
        }
        this.bsModalRef.hide();
    }

    ngOnDestroy() {
        console.log('DESTROY');
        this.destroyed$.next();
        this.destroyed$.complete();
    }
}

Как видим, здесь есть отписка (takeUntil). Всё как советовал нам “учитель”. Также здесь есть большой массив.

Код GoodModalComponent
@Component({
  selector: 'app-good-modal',
  templateUrl: './good-modal.component.html',
  styleUrls: ['./good-modal.component.css']
})
export class GoodModalComponent implements OnInit, OnDestroy {

    loading = false;
    largeData: number[] = (new Array(1000000)).fill(1);
    data: DataInterface;

    constructor(
        private http: HttpClient,
        private bsModalRef: BsModalRef
    ) {
    }

    ngOnInit() {
        this.loadData();
    }

    loadData(): void {
        // For example only, not for production.

        this.loading = true;
        const subscription = this.http.get<DataInterface>('/action.php?a=1').pipe(
            catchError((err) => throwError(err.message)),
            finalize(() => console.log('FINALIZE'))
        )
            .subscribe({
                next: (res) => {
                    setTimeout(() => {
                        console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!');
                    }, 0);
                    console.log('LOADED');
                    this.data = res;
                    this.loading = false;
                },
                error: (error) => {
                    setTimeout(() => {
                        console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!');
                    }, 0);
                    console.log('ERROR', error);
                },
                complete: () => {
                    setTimeout(() => {
                        console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!');
                    }, 0);
                    console.log('COMPLETED');
                }
            });
    }

    close(event?: MouseEvent): void {
        if (event) {
            event.preventDefault();
        }
        this.bsModalRef.hide();
    }

    ngOnDestroy() {
        console.log('DESTROY');
    }
}

Здесь есть точно такое же свойство с большим массивом, но отписки нет. И это не мешает мне именно это окошко назвать хорошим. Почему — позже.

Смотрим видео:

Как видно, в обоих случаях после перехода на второй компонент сборщик мусора успешно вернул память к нормальным значениям. Да, можно было сделать возможность очистки памяти также после закрытия окошек, но в нашем эксперименте это не важно. Выходит, что “учитель” был не прав, когда говорил:

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

Да, он говорит про “потенциальную” утечку. Но, если поток конечный, то утечки памяти не будет.

Предвижу возмущенные возгласы подобных “учителей”. Они нам обязательно скажут что-то вроде: “ок, утечки памяти нет, но отпиской мы так же отменяем запрос, а значит мы будем уверены, что не будет больше выполняться никакой код после получения ответа от сервера”. Во-первых, я не говорю, что отписка это всегда плохо, я лишь говорю, что вы подменяете понятия. Да, то, что после прихода ответа выполнится ещё какая-то бесполезная операция — это плохо, но защититься от реальной утечки памяти можно только отпиской (в данном случае), а защититься от других нежелательных эффектов можно другими способами. Не нужно запугивать читателей и навязывать им свой стиль написания кода.

Другие подписки:  Ketoplan списали деньги: как отключить подписку

Всегда ли нам нужно отменять запрос, если пользователь передумал? Не всегда! Не забывайте, что запрос-то вы отмените, но вы не отмените выполнение операции на сервере. Представим, что пользователь открыл какой-то компонент, там долго что-то грузится и он переходит на другой компонент. Возможны ситуации, что сервер загружен и не справляется со всеми запросами и операциями. В этом случае пользователь может начать судорожно тыкать все ссылки в навигации и создавать ещё бОльшую нагрузку серверу, потому что выполнение запроса не останавливается на стороне сервера (в большинстве случаев).

Смотрим следующее видео:

Я заставил пользователя дождаться ответа. В большинстве случаев ответ придет быстро и пользователь не будет испытывать неудобств. Но таким способом мы убережем сервер от выполнения повторных тяжелых операций, если такие возникнут.

Итоги:

  • Я не говорю, что отписываться от подписок RxJS запросов HttpClient не нужно. Я лишь говорю, что бывают случаи когда этого делать не нужно. Не нужно подменять понятия. Если вы говорите про утечку памяти, покажите эту утечку. Не ваши бесконечные console.log, а именно утечку. Память в чём измеряется? Время выполнения операции в чём измеряется? Вот это и нужно показать.
  • Я не называю своё решение, которое я применил в тестовом приложении, “серебряной пулей”. Наоборот, я призываю дать фронтендеру больше свободы. Пусть он сам принимает решение как ему писать свой код. Не нужно его запугивать и навязывать свой стиль разработки.
  • Я против фанатизма и преждевременной оптимизации. Этого в последнее время вижу слишком много.
  • В браузере есть более продвинутые методы поиска утечек памяти, чем тот, который я показал. Считаю в моем случае применение этого простого способа достаточным. Но рекомендую ознакомиться с темой более подробно, например, в этой статье: https://habr.com/ru/post/309318/.

UPD #1
На данный момент пост провисел почти сутки. Сначала он уходил то в плюсы, то в минусы, потом оценка остановилась на нуле. Это значит, что аудитория разделилась ровно на два лагеря. Не знаю хорошо это или плохо.

UPD #2
В комментариях объявился Реактивный Лис (автор разбираемой статьи). Сначала он меня поблагодарил, был очень вежлив. Но, увидев пассивность аудитории, принялся прессинговать. Дошло до того, что он написал, что я должен извиниться. Т.е. наврал он (враньё выделено желтой рамочкой выше), а извиниться должен я.
Сначала я думал, что перехватчик потоков с бесконечными повторами (ладно бы 2-3 повтора), который он написал в своем демо-приложении, только для тестов и информирования. Но оказалось, что он его считает примером из жизни. Т.е. блокировать кнопочку окошка — нельзя. А создавать подобные перехватчики, нарушая принципы SOLID, нарушая модульность приложения (модули и компоненты должны быть независимыми друг от друга), пуская лесом unit-тесты ваших юнитов (компонентов, сервисов) — можно. Представьте ситуацию: Написали вы компонент, написали юнит-тесты к нему. А потом появляется такой Лис, добавляет в ваше приложение подобный перехватчик и ваши тесты становятся бесполезными. Потом он ещё заявляет вам: “А чё это ты не предугадал то, что я могу захотеть добавить такой перехватчик. Ну-ка исправляй свой код”. Возможно это может быть реальностью в его команде, но я не считаю, что такое нужно поощрять или закрывать на это глаза.

UPD #3
В комментариях в основном обсуждают подписки и отписки. Разве пост называется “Отписка — зло”? Нет. Я не призываю вас не делать отписок. Делайте так же, как делали раньше. Но вы должны понимать почему вы это делаете. Отписка не является преждевременной оптимизацией. Но, вступая на путь защиты от потенциальных угроз (как призывает нас автор разбираемой статьи), вы можете переступить черту. Тогда ваш код может стать перегруженным и сложно поддерживаемым.
Эта статья про фанатизм, к которому приводит распространение непроверенной информации. Относиться к отсутствию отписки в некоторых случаях нужно более спокойно (нужно чётко понимать существует ли проблема в конкретном случае).

UPD #4

Наоборот, я призываю дать фронтендеру больше свободы. Пусть он сам принимает решение как ему писать свой код.

Тут нужно уточнить. Я за стандарты. Но стандарт может установить автор библиотеки или его команда, пока этого нет (в документации и официально). Например, в документации фреймворка Symfony есть раздел Best practices. Если бы такой же был бы в документации RxJS и там было бы написано “подписался — отпишись”, у меня не возникло бы желания с ним спорить.

UPD #5
Важный комментарий с ответами от авторитетных людей:
https://podpiski-help.ru/ru/post/479732/#comment_21012620
Рекомендация исполнять контракт “подписался — отпишись” от разработчика RxJS существует, но неофициально.

— изменить 2 (2022/12/28)

Источник 5

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

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

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

TLDR:

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

— редактировать 1

Источник 4

— редактировать 3 – «официальное» решение (2022/04/09)

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

Другие подписки:  Как отключить платную подписку ivi - основные способы

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

— редактировать 4 – дополнительные ресурсы (2022/09/01)

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

Javascript – отписаться от rxjs observables –

У меня есть эти два объекта, и я хочу перестать слушать их события. Я совершенно новичок в наблюдаемых и RxJS, и просто пытаюсь работать с библиотекой Inquirer.

Вот API RxJS для справки: http://reactivex.io/rxjs/class/es6/Observable.js ~ Observable.html

Как я могу отписаться от этих типов наблюдаемых?

< Сильный > ConnectableObservable :

   ConnectableObservable {
     source: EventPatternObservable { _add: [Function], _del: [Function], _fn: undefined },
     _connection: ConnectDisposable { _p: [Circular], _s: [Object] },
     _source: AnonymousObservable { source: [Object], __subscribe: [Function: subscribe] },
     _subject: 
      Subject {
        isDisposed: false,
        isStopped: false,
        observers: [Object],
        hasError: false } },
  _count: 1,
  _connectableSubscription: 
   ConnectDisposable {
     _p: 
      ConnectableObservable {
        source: [Object],
        _connection: [Circular],
        _source: [Object],
        _subject: [Object] },
     _s: AutoDetachObserver { isStopped: false, observer: [Object], m: [Object] } } }

< Сильный > FilterObservable :

FilterObservable {
  source: 
   RefCountObservable {
     source: 
      ConnectableObservable {
        source: [Object],
        _connection: [Object],
        _source: [Object],
        _subject: [Object] },
     _count: 1,
     _connectableSubscription: ConnectDisposable { _p: [Object], _s: [Object] } },
  predicate: [Function] }

Мне нужно отписаться от этих объектов:

'use strict';
var rx = require('rx');

function normalizeKeypressEvents(value, key) {
  return {value: value, key: key || {}};
}

module.exports = function (rl) {

  var keypress = rx.Observable.fromEvent(rl.input, 'keypress', normalizeKeypressEvents)
    .filter(function (e) {
      // Ignore `enter` key. On the readline, we only care about the `line` event.
      return e.key.name !== 'enter' && e.key.name !== 'return';
    });

  return {
    line: rx.Observable.fromEvent(rl, 'line'),

    keypress: keypress,

    normalizedLeftKey: keypress.filter(function (e) {
      return e.key.name === 'left';
    }).share(),

    normalizedRightKey: keypress.filter(function (e) {
      return e.key.name === 'right';
    }).share(),

    normalizedUpKey: keypress.filter(function (e) {
      return e.key.name === 'up' || e.key.name === 'k' || (e.key.name === 'p' && e.key.ctrl);
    }).share(),

    normalizedDownKey: keypress.filter(function (e) {
      return e.key.name === 'down' || e.key.name === 'j' || (e.key.name === 'n' && e.key.ctrl);
    }).share(),

    numberKey: keypress.filter(function (e) {
      return e.value && '123456789'.indexOf(e.value) >= 0;
    }).map(function (e) {
      return Number(e.value);
    }).share(),

    spaceKey: keypress.filter(function (e) {
      return e.key && e.key.name === 'space';
    }).share(),

    aKey: keypress.filter(function (e) {
      return e.key && e.key.name === 'a';
    }).share(),

    iKey: keypress.filter(function (e) {
      return e.key && e.key.name === 'i';
    }).share()
  };
};

Моя текущая лучшая догадка заключается в том, что явного вызова подписки не происходит, как это:

var source = Rx.Observable.fromEvent(input, 'click');

var subscription = source.subscribe(
  function (x) {
    console.log('Next: Clicked!');
  },
  function (err) {
    console.log('Error: %s', err);
  },
  function () {
    console.log('Completed');
  });

Но вместо этого есть следующие вызовы:

events.normalizedUpKey.takeUntil(validation.success).forEach(this.onUpKey.bind(this));
events.normalizedDownKey.takeUntil(validation.success).forEach(this.onDownKey.bind(this));

Так что мое лучшее предположение, что мне нужен способ обнулить / отменить вызов takeUntil.

Нет необходимости и нет протокола для отписки от наблюдаемого. На самом деле, я вижу код в вашем вопросе, особенно ту часть возвращаемого объекта, в которую была включена группа наблюдаемых, составленная из общего ресурса. Однако эти наблюдаемые все еще являются наблюдаемыми, а не подпиской, что означает, что для этих элементов не существует такого понятия, называемого unsubscribing.

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

В настоящее время для кода, о котором идет речь, методы, используемые в наблюдаемом источнике, являются всеми операторами, такими как .filter().share().takeUntil(), а не подпиской на выполнение, которые на самом деле являются методами, возвращающими новые наблюдаемыми.

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

В заключение, вам не нужно беспокоиться о том, чтобы отменить подписку на ваш код в вашем вопросе. Потенциально существует еще одна проблема, которая звучит как проблема, описанная в вашем вопросе: если вы используете .connect() вместо .share(). В ConnectableObservable возникает ситуация, в которой вам нужно беспокойство о ручной отмене привязки событий.

Другие операторы

Есть много других способов остановить поток в «Rx-way». Я бы рекомендовал глянуть следующие операторы как минимум:

take(n): берет N значений перед остановкой наблюдаемого.takeWhile(предикат): проверяет пропускаемые через себя значения на предикат, если он возвращает ложь, поток будет завершен.first(): пропускает первое значение и завершает работу.first(предикат): проверяет каждое значение на функцию предиката, если он возвращает истину, поток пропускает значение и завершается.

Загадка на посошок

Если у вас есть желание немного поупражняться, решите предыдущую задачу, но для создания продукта. То есть сначала создаём продукт, потом обновляем теги, а только потом — категории.

Использование observable для автокомплита или поиска

Задача: Показывать предложения страниц при вводе данных на форме

Решение: Подпишемся на изменение данных формы, возьмём только меняющиеся данные инпута, поставим небольшую задержку, чтобы событий не было слишком много и отправим запрос в википедию. Результат выведем в консоль. Интересный момент в том, что switchMap отменит предыдущий запрос, если пришли новые данные.

Использование takeuntil для отписки

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

Как выглядит императивное управление подпиской


Возьмем, к примеру, этот выдуманный компонент (это специально не React и не Angular, а просто общий пример):

class MyGenericComponent extends SomeFrameworkComponent {
 updateData(data) {
  // что-нибудь специфичное для обновления компонента
 }

 onMount() {
  this.dataSub = this.getData()
   .subscribe(data => this.updateData(data));

  const cancelBtn = this.element.querySelector(‘.cancel-button’);
  const rangeSelector = this.element.querySelector(‘.rangeSelector’);

  this.cancelSub = Observable.fromEvent(cancelBtn, ‘click’)
   .subscribe(() => {
    this.dataSub.unsubscribe();
   });

  this.rangeSub = Observable.fromEvent(rangeSelector, ‘change’)
   .map(e => e.target.value)
   .subscribe((value) => {
    if ( value > 500) {
      this.dataSub.unsubscribe();
    }
   });
 }

 onUnmount() {
  this.dataSub.unsubscribe();
  this.cancelSub.unsubscribe();
  this.rangeSub.unsubscribe();
 }
}

В приведенном выше примере вы можете увидеть, что я вручную вызываю `unsubscribe` на трех объектах подписки в методе `onUnmount()`. Также я вызываю `this.dataSub.unsubscribe()`, когда пользователь нажимает кнопку отмены в строках 15 и 22, или когда он устанавливает селектор диапазона выше 500, что является некоторым порогом, на котором я хочу остановить поток данных. (Не знаю, зачем, это просто странный компонент).

Другие подписки:  Английский язык. Факультатив — Центр развития образовательных технологий — Национальный исследовательский университет «Высшая школа экономики»

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

Единственным реальным преимуществом использования этого подхода является производительность. Поскольку вы используете меньше абстракций, чтобы выполнить свою работу, это, скорее всего, будет немного лучше. Однако это вряд ли будет иметь заметный эффект в большинстве веб-приложений, и я не думаю, что об этом стоит беспокоиться.

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

Кеширование запроса

Задача: Необходимо закешировать Observable запрос

Остановка анимации загрузки после окончания выполнения подписки

Задача: Показать значок загрузки после начала сохранения данных и скрыть его, когда данные сохранятся или произойдет ошибка.

Решение: За отображение загрузчика у нас отвечает переменная loading, после нажатия на кнопку, установим ее в true. А для установки ее в false воспользуемся Observable.finally функций, которая выполняется после завершения подписки или если произошла ошибка.

Последовательный combinelatest

Задача: Критическая ситуация на сервере! Backend команда сообщила, что для корректного обновления продукта нужно выполнять строго последовательно:

  1. Обновление данных продукта (заголовок и описание);
  2. Обновление списка тегов продукта;
  3. Обновление списка категорий продукта;

Решение: У нас есть 3 Observable, полученных из productService. Воспользуемся concatMap:

const updateProduct$ = this.productService.update(product);
const updateTags$ = this.productService.updateTags(productId, tagList);
const updateCategories$ = this.productService.updateCategories(productId, categoryList);

Observable
  .from([updateProduct$, updateTags$, updateCategories$])
  .concatMap(a => a)  // выполняем обновление последовательно
  .toArray()          // Возвращает массив из последовательности
  .subscribe(res => console.log(res)); // res содержит массив результатов запросов

Резюме: используйте takeuntil, takewhile и пр.

Вероятно, вы должны использовать такие операторы, как `takeUntil`, чтобы управлять подписками в RxJS. Как правило, если вы видите, что есть две или больше подписки в одном компоненте, вы должны задаться вопросом, можете ли вы определить их лучше:

Скомпонуйте управление подписками с помощью takeuntil


Теперь давайте сделаем тот же пример, но используя оператор `takeUntil` из RxJS:

class MyGenericComponent extends SomeFrameworkComponent {
 updateData(data) {
  // do something framework-specific to update your component here.
 }

 onMount() {
   const data$ = this.getData();
   const cancelBtn = this.element.querySelector(‘.cancel-button’);
   const rangeSelector = this.element.querySelector(‘.rangeSelector’);
   const cancel$ = Observable.fromEvent(cancelBtn, 'click');
   const range$ = Observable.fromEvent(rangeSelector, 'change')
                            .map(e => e.target.value);
   
   const stop$ = Observable.merge(cancel$, range$.filter(x => x > 500))
   this.subscription = data$.takeUntil(stop$)
                            .subscribe(data => this.updateData(data));
 }

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

Первое, что вы могли заметить, это меньший объем кода. Но это лишь одно преимущество. Еще одна вещь, которая произошла здесь, заключается в том, что я скомпоновал в поток `stop$` события, которые останавливают поток данных. Это означает, что как только я решу, что хочу добавить еще одно условие, чтобы остановить поток, например по таймеру, я могу просто добавить новый наблюдаемый объект в `stop$`.

Следующая очевидная вещь — у меня есть только один объект подписки, которым я управляю императивно. Этого не изменишь, так как здесь функциональное программирование пересекается с объектно-ориентированным миром. Javascript — это язык императивный и нам приходится приходится принимать остальной мир в каком-то смысле наполовину.

Другим преимуществом этого подхода является то, что он, фактически, завершает наблюдаемый объект. Это означает, что возникнет событие завершения, которое можно обрабатать в любое время. Если вы просто вызываете `unsubscribe` на возвращенном объекте-подписке, вы не будете уведомлены о том, что подписка была отменена.

Последнее преимущество, о котором я хочу сказать, заключается в том, что вы, на самом деле, «подключаете все», вызывая подписку в одном месте, и это хорошо, потому что с дисциплиной становится намного легче найти, где вы начинаете подписки в вашем коде.

Правда, есть один недостаток в терминах семантики RxJS, но едва ли стоит беспокоиться перед другими преимуществами. Семантический недостаток заключается в том, что завершение наблюдаемого объекта — это признак того, что производитель хочет сказать потребителю, что дело сделано, в то время как отказ от подписки — это потребитель, говорящий производителю, что ему больше не нужны данные.

Также будет очень небольшой разница в производительности между этим и простым императивным вызовом `unsubscribe`. Однако маловероятно, что она будет заметна в большинстве приложений.

Создание собственного источника событий

Задача: Создать переменную lang$ в configService, на которую другие компоненты будут подписываться и реагировать, когда язык будет меняться.

Решение: Воспользуемся классом BehaviorSubject для создания переменной lang$;

ОтличияBehaviorSubject от Subject:

  1. BehaviorSubject должен инициализироваться с начальным значением;
  2. Подписка возвращает последнее значение Subjectа;
  3. Можно получить последнее значение напрямую через функцию getValue().

Создаём переменную lang$ и сразу инициализируем. Так же добавляем функцию setLang для установки языка.

// configService
lang$: BehaviorSubject<Language> = new BehaviorSubject<Language>(DEFAULT_LANG);
setLang(lang: Language) {
  this.lang$.next(this.currentLang); // тут мы поставим
}

Подписываеся на изменение языка в компоненте. Переменная lang$ является “горячим” Observable объектом, то есть подписка требует отписки при разрушении объекта.

private subscriptions: Subscription[] = [];
ngOnInit() {
  const langSub = this.configService.lang$
    .subscribe(() => {
      // ...
    });
  this.subscriptions.push(langSub);
}
ngOnDestroy() {
  this.subscriptions
    .forEach(s => s.unsubscribe());
}

Оцените статью
Подписки Help
Добавить комментарий