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



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



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




Продолжим читать статью человека под ником Reactive Fox:



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


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


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


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



Как подтверждение того, что нужно всегда отписываться от подписок Observable запросов HttpClient, он добавил такой перехватчик запросов, который нам выводит в консоль "still alive… still alive… still alive...":

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


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


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


Код action.php
<?php

header('Content-Type: application/json');

function logging($str, $fileName = 'log.txt')
{
    if (is_array($str)) {
        $str = json_encode($str);
    }
    $rootPath = __DIR__;
    $logFilePath = $rootPath . DIRECTORY_SEPARATOR . $fileName;

    $options = [
        'max_log_size' => 200 * 1024
    ];
    if (!is_dir(dirname($logFilePath))) {
        mkdir(dirname($logFilePath));
    }
    if (file_exists($logFilePath) && filesize($logFilePath) >= $options['max_log_size']) {
        unlink($logFilePath);
    }

    $fp = fopen( $logFilePath, 'a' );
    $dateFormat = 'd/m/Y H:i:s';
    $str = PHP_EOL . PHP_EOL . date($dateFormat) . PHP_EOL . $str;

    fwrite( $fp, $str );
    fclose( $fp );

    return true;
}

$actionName = isset($_GET['a']) && !is_array($_GET['a']) ? $_GET['a'] : '1';

logging("STARTED-{$actionName}");

sleep(3);// Very resource-intensive operation that takes 3 seconds

logging("COMPLETED-{$actionName}");

echo json_encode([
    'success' => true,
    'data' => ['name' => 'test', 'title' => 'This is a test']
]);

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


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


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


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

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


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



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


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

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


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


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


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



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


Итоги:


  • Я не говорю, что отписываться от подписок 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://habr.com/ru/post/479732/#comment_21012620
Рекомендация исполнять контракт "подписался — отпишись" от разработчика RxJS существует, но неофициально.

Комментарии (55)


  1. khim
    13.12.2019 15:18

    По-моему вместо всей этой тирады достаточно процитировать определение:

    Уте?чка па?мяти (англ. memory leak) — процесс неконтролируемого уменьшения объёма свободной оперативной или виртуальной памяти компьютера, связанный с ошибками в работающих программах, вовремя не освобождающих ненужные участки памяти, или с ошибками системных служб контроля памяти.

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

    Всё. Ну вот совсем всё.

    Остальное — это детали…


    1. Andchir Автор
      16.12.2019 02:12

      Если нет (скажем ваше приложение всё равно каждый раз при нажатии на кнопку запрашивает данные с сервера, так как считается, что они ну очень бысто там меняются) — то, разумеется, утечка. И показать как она может сожрать гигабайты памяти будет, в таком приложении, совсем несложно.
      Не понятно, Вы про мой пример или нет. Если про мой, то покажите утечку, если это «совсем несложно», как говорите.


  1. thekiba
    13.12.2019 15:45
    +1

    Спасибо за детальный разбор статьи и примера!

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

    Соблюдение контракта «подписался и отписался» позволяет нам меньше попадать в ситуацию, когда приложение начинает работать не так, как предполагалось изначально. В пример могу привести включение HMR, когда особо важно, чтобы приложение уничтожило неиспользуемые ресурсы, или внезапное переключение контекста, когда вместо ожидаемых значений или поведений у нас происходит что-то иное.

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

    Сейчас я могу лишь предположить, что вы немного неправильно восприняли основной посыл статьи Почему вам НАДО отписываться от Observable. Основная цель в том, чтобы привлечь внимание к данному явлению и показать некоторые способы решения этой проблемы.

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

    И самое главное, takeUntil должен быть в конце:

    // Wrong!
    pipe(
      takeUntil(read),
      switchMapTo(story)
    );
    
    // Ok!
    pipe(
      switchMapTo(story),
      takeUntil(read)
    );
    


    1. Andchir Автор
      13.12.2019 16:05

      Соблюдение контракта «подписался и отписался» позволяет нам...
      Правило должно быть таким: «подписался на бесконечный стрим — отпишись когда он больше не нужен». Просто и понятно. Всё остальное это ваше личное решение и принято оно может быть в зависимости от конкретной ситуации.


      1. thekiba
        13.12.2019 16:18

        Когда мы работаем с Observable, то у нас нет понимая на уровне типов является ли поток бесконечным или конечным, холодным или горячим. Более того, бывает, что могут произойти ситуации, когда код будет работать неправильно, потому что забыли отпустить старый ресурс.

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


  1. GCU
    13.12.2019 15:54

    Observable, которые создает HttpClient Angular, т.к. там после прихода ответа или в случае ошибки отписка (complete) происходит автоматически.

    Ну это как-бы частный случай Observable, и где гарантии что там ответ конечный?


    1. Andchir Автор
      13.12.2019 16:09

      Гарантии в коде HttpClient Angular.


      1. Rem1te
        13.12.2019 16:35
        +1

        HttpClient не более чем частный и эксклюзивный случай, ведь в статье вы отметили что речь идет о «Observable RxJS», а не о библиотеке фреймворка.
        К тому же завтра реализация HttpClient поменяется на очередной HttpClientClient и гарантий что эти потоки будут закрываться самостоятельно не будет никакой.

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


      1. GCU
        13.12.2019 17:03
        +1

        Увы, никаких ограничений на конечность не нашёл. Не могли бы подсказать куда нужно смотреть?
        Я имею ввиду тот случай, когда сервер реально отвечает безразмерным/бесконечным потоком.


  1. JustDont
    13.12.2019 16:30

    Не в контексте RxJS, а в целом — ага, я уже успел повидать прилично реактивного кода, который отписывается совсем даже не по делу. Например, классика (MobX) — внутри объекта есть какие-то реактивные сайд-эффекты, которые действуют только внутри этого объекта. И тем не менее — рядышком лежит пространный и абсолютно бессмысленный код по отписке и повторной подписке. А этого кода могло бы просто не быть, т.к. время жизни реакций в любом случае не будет превышать времени жизни самого объекта, в котором они находятся.


    1. Xuxicheta
      13.12.2019 17:19
      +1

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


      1. JustDont
        13.12.2019 18:16

        зато вы избегаете трудноотслеживаемых эффектов, которые возможны если бы отписки не было

        Очень смешно. Какие еще «трудноотслеживаемые эффекты» в подписках, которые прям в этом же коде и объявлены?
        Я б еще понял, если б речь шла про присылаемое через DI и объявленное хрен знает где.


        1. Xuxicheta
          13.12.2019 19:07

          поток может иметь сайд эффекты и наоборот, дополнительные источники далеко от местного кода.


          1. JustDont
            13.12.2019 19:10

            В MobX не потоки, а состояния.
            Но в любом случае, это абсолютно бессмысленное метание аргументами — я вам сразу скажу, что в JS (да и во многих других языках тоже) я вам каким-то посторонним кодом при желании сломаю ваш код без каких-либо проблем. И не важно, что там у вас — потоки, не потоки, браузер, не браузер, неважно.

            Нет, вывод из вышенаписанного — вовсе не «а давайте защищаться всеми возможными и невозможными способами от вероятностей того, что кто-то со стороны сломает наш код».


    1. ganqqwerty
      13.12.2019 18:51

      А покажите ссылку на этот кусок в MobX? Я думаю, что тогда предметнее получится пообсуждать его — да и у разработчиков спросить, почему так — вдруг им видны исключительные ситуации, которые не видны нам.


      1. JustDont
        13.12.2019 19:02

        А покажите ссылку на этот кусок в MobX?

        Как я вам её покажу — это корпоративный код.
        Или вы как-то ухитрились прочитать мои слова так, что решили, что в самой MobX есть проблемы?


  1. ganqqwerty
    13.12.2019 16:36

    Мне кажется, что отписываться от потоков надо просто потому что тогда вам не надо будет волноваться — конечный он или бесконечный. В приложениях с активным использованием rx вы не всегда можете это понять с первого взгляда.


    1. Andchir Автор
      13.12.2019 16:50
      -1

      В приложениях с активным использованием rx вы не всегда можете это понять с первого взгляда.
      Всегда. 1. Логика. 2. Временно добавить в код проверку:
      ...
      .subscribe({
          next: (res) => {
              ...
          },
          error: (error) => {
              setTimeout(() => {
                  console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!');
              }, 0);
          },
          complete: () => {
              console.log('COMPLETED');
          }
      });


      1. thekiba
        13.12.2019 17:25

        Не совсем понимаю, что именно вы хотели показать данным примером.

        С точки зрения правильности он абсолютно неверен, потому что когда Observable эмитит error или complete, то поток завершается всегда. И все что можно сделать в этом случае, это попросту создать новый поток или ничего не делать.

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


        1. Andchir Автор
          13.12.2019 18:51

          Хотел показать как увидеть завершение потока.


          1. ganqqwerty
            13.12.2019 18:52

            Я так понимаю, это просто отладочный прием, а не код, который пойдет в продакшн?


            1. Andchir Автор
              13.12.2019 19:06

              Да, я же написал «временно добавить». Это и есть — снять метрики, если в чём-то не уверены, в соответствии с советами Дональда Кнута. Вы с ним не согласны? Если в чём-то не уверены, всегда пишите дополнительный код без проверки?


              1. ganqqwerty
                13.12.2019 19:24

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


      1. ganqqwerty
        13.12.2019 18:44

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

        Наружу мы видим имя переменной и ее тип — reports$: Observable\<DailyReport\> — непонятно, конечный reports или нет. Залезем внутрь, продеремся через цепочки forkJoin'ов, iif'ов и прочих combineLatest' ов (все это — внутри ngrx'овой экосистемы), определим все зависимости нашего reports и наконец найдем — ага, один из потоков, из которого был сформирован $reports — бесконечный!
        Ну что ж, значит, надо отписываться.
        Или наоборот — вы посмотрели на все потоки, из которых порожден reports и поняли, что reports — конечный. Вот вы и не отписываетесь — незачем же. А как долго он будет оставаться конечным в процессе разработки? Не может ли случиться так, что один из порождающих потоков будет заменен на другой — бесконечный — и вы забудете, что надо бы теперь reports отписываться?
        Вот и получается cost-benefit-анализ: сколько нам стоит отписаться от того, что сейчас и так конечное против сколько нам будет стоить не отписаться от того, что было конечным, а стало бесконечным. Я утверждаю, что в этом случае перестраховаться и отписаться будет дешевле во всех смыслах.


        1. Andchir Автор
          13.12.2019 19:04

          Если вы в своем пайпе не делаете никакой подписки на события DOM элементов и не добавляете повторы, то он будет конечным. Вы должны держать под контролем свой код. А если вы делаете в своем пайпе всякие мержи, где комбинируете конечные и бесконечные подписки, скорее всего вам стоит задуматься о читаемости и простоте поддержки вашего кода. Почему вы так уверены, что написали не говнокод? Просто потому что это RxJS?


          1. ganqqwerty
            13.12.2019 19:15

            Я не понял этого тезиса. Вы думаете, что комбинировать конечные и бесконечные потоки — это какая-то редкость? вот товарищ Xuxicheta постом ниже привел пример обычного switchMap'а, которым пользуются все, и который возьмет и сделает отписку обязательной.
            Я в процессе ответа, пытался вспомнить, что у нас за штуки бывают бесконечными. Навспоминал WebWorker'ы, EventSource, DOM-события, таймеры и разные виды reply'ев. Уверен, что что-то забыл. С вашей философией нужно распечатать полный список и повесить для команды на видном месте — чтобы каждый раз все всё проверяли, а то не дай бог без нужды отпишутся и засорят кодовую базу.


            1. Andchir Автор
              13.12.2019 19:40

              Вам такой же ответ как здесь:
              habr.com/ru/post/479732/#comment_21006040


        1. Xuxicheta
          13.12.2019 19:04
          +1

          да легко. Сегодня метод возвращает холодный http request, а завтра к нему добавили какой нибудь Map оператор, который переключает наш реквест на другой поток и ой, он уже не завершается.

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


          1. Andchir Автор
            13.12.2019 19:10

            «а завтра...» — читаем про преждевременную оптимизацию.
            «Или еще вариант, наш поток порожден из какой-то другой подписки...» — читаем про SOLID.


            1. Xuxicheta
              13.12.2019 19:15
              +1

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

              Да и вообще на руках не может быть ничего вредоносного, читаем руководство «как не запачкать руки», и в будущем таких ситуаций может и не быть, читаем «как перестать беспокоиться и не мыть руки»


              1. Andchir Автор
                13.12.2019 19:23

                Ещё раз. Я не говорил, что отписывать и завершать запрос — это всегда плохо. Я наоборот выступаю против этого «всегда». В статье приведен конкретный пример, когда отписка не нужна. Она там реально не нужна и будет лишним кодом. Не согласны? Вот с этим и спорьте. Не надо мне приписывать то, чего я не говорил.


                1. ganqqwerty
                  13.12.2019 20:01

                  Я вот не согласен именно с тем, что она когда-либо может быть не нужна. Возьмем уж совсем простой случай. Есть у меня компонент, он зависит от сервиса с функцией, возвращающей Observable\<DailyReport\>:

                  myService.getDailyReport().subscribe((report) => {this.report = report;})
                  

                  Надо ли от такого отписываться? Мой ответ — надо, посколько тогда мы будем следовать принципам инкапсуляции. Наружу торчит некоторая Observable, как она получилось — мне, как компоненту, вредно знать. Может быть, она получилась просто из http.get, может быть — из EventSource, может быть, ее наколдовал рогатый черт — и меня это не должно волновать. Меня волнует только то, что это Observable, а Observable'ы бывают бесконечными

                  Вы же нарушаете инкапсуляцию, настаивая на том, что я должен пойти и почитать код функции getDailyReport(), после чего заключить — «а, ну там же просто http.get, не буду отписываться, это лишнее».

                  Очень хорошо принципам инкапсуляции учит C++, где наружу может торчать только .h-файл, а сама логика будет спрятана в dll-ке. Вот представьте, что код getDailyReport() до безобразия обфусцирован и не подлежит прочтению пока вы находитесь в своем компоненте. Это улучшит код.


                  1. Andchir Автор
                    13.12.2019 20:05

                    В вашем конкретном случае — да — нужно. Разве я говорил, что не нужно всегда? Ещё раз перечитайте предыдущий мой ответ. Я выступаю против фанатизма и подмены понятий. Привел конкретные примеры, а вы из меня лепите какого-то нового «гуру», который учит жизни. Я как раз против такого. Читайте выводы внизу статьи.


                    1. thekiba
                      13.12.2019 21:45

                      Давайте пройдемся по выводам статьи, у меня к ним очень много вопросов.

                      Я не говорю, что отписываться от подписок RxJS запросов HttpClient не нужно. Я лишь говорю, что бывают случаи когда этого делать не нужно.


                      На сколько мы можем быть уверены в том, что частные случаи всегда будут неизменяемыми, например «делать отписки не нужно» не превратятся в «делать отписки нужно»?

                      Сколько потребуется затратить времени на поиск возникшей ошибки, ее описание, локализацию, исправление или полное переписывание всего участка кода?

                      Будет ли возможность исправить эту проблему, если на уровне архитектуры не закладывалась отмена потоков?

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

                      Не нужно подменять понятия. Если вы говорите про утечку памяти, покажите эту утечку. Не ваши бесконечные console.log, а именно утечку. Память в чём изменяется? Время выполнения операции в чём измеряется? Вот это и нужно показать.


                      Почему обычный console.log не подходит для демонстрации проблемы?

                      Что мешает взять готовый пример с проблемой и самому попробовать открыть Developer Tools, запустить профилировку и увидеть все собственными глазами?

                      Если приведенные мной примеры не являются удачными, то давайте сделаем их лучше? Я только за!

                      Почему вы считаете, что происходит подмена понятий, если описанная в статье проблема решается простым способом — через отписку?

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


                      В чем заключается подмена понятий, когда мы говорим: Необходимо отписываться, чтобы предостеречь себя от ситуаций, когда код будет работать неправильно? Ведь проблема заключается не только в потенциальных утечках памяти и спада производительности, но и в различных сайд эффектах, которые могут произойти.

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


                      В чем именно заключается запугивание и ограничение в свободе, о которой вы говорите?

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

                      И, простите, но вот как раз из-за подобных решений и была написана та статья с примерами: habr.com/ru/post/479732/#comment_21004844. Вы также можете открыть группу Angular в телеграмме и поискать еще подобных изысков, найдете много интересного.

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

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

                      Я против фанатизма и преждевременной оптимизации. Этого в последнее время вижу слишком много.


                      Смотрите, программа должна реализовывать в себе несколько простых вещей: безопасно включиться и выключиться, корректно работать, и быть устойчивой к изменениям со стороны. То же самое и с потоками. Используя RxJS мы прямо из коробки реализуем эту функциональность. В чем именно тут заключается преждевременная оптимизация?

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

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


                      1. Andchir Автор
                        13.12.2019 22:35

                        Давайте пройдемся по выводам статьи, у меня к ним очень много вопросов.
                        Так не пойдет. Проходиться нужно не по выводам, а по всему тексту статьи. Тогда многие вопросы отпадут сами собой.
                        Почему обычный console.log не подходит для демонстрации проблемы?
                        Что мешает взять готовый пример с проблемой и самому попробовать открыть Developer Tools, запустить профилировку и увидеть все собственными глазами?
                        Я открыл, проверил и оказалось, что ваши размышления не имеют ничего общего с реальностью. Подробности есть в статье.
                        Если приведенные мной примеры не являются удачными, то давайте сделаем их лучше? Я только за!
                        Тут я вам не помощник, потому что по моему мнению вы пытаетесь доказать то, чего нет. Точнее называете утечкой то, что ей не является.
                        лучше отписываться от потоков, когда они больше не нужны, так как рано или поздно это приводит к проблемам
                        Приведите конкретный пример. Можно даже из реального проекта. Не надо заставлять читателя поверить вам на слово или придумывать пример самостоятельно. Хватит пустословия! Больше конкретики. Покажите код, где ваша методика предугадывания проблем пригодилась.

                        Прошу прощения, если что-то пропустил. Позже проверю, пока нет времени.


                        1. thekiba
                          13.12.2019 23:43

                          Так не пойдет. Проходиться нужно не по выводам, а по всему тексту статьи. Тогда многие вопросы отпадут сами собой.


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

                          Выше вы сказали:
                          Я открыл, проверил и оказалось, что ваши размышления не имеют ничего общего с реальностью. Подробности есть в статье.


                          Тут я вам не помощник, потому что по моему мнению вы пытаетесь доказать то, чего нет. Точнее называете утечкой то, что ей не является.


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


                          По поводу примеров:
                          Приведите конкретный пример. Можно даже из реального проекта. Не надо заставлять читателя поверить вам на слово или придумывать пример самостоятельно. Хватит пустословия! Больше конкретики. Покажите код, где ваша методика предугадывания проблем пригодилась.


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

                          Из комментария ниже habr.com/ru/post/479732/?reply_to=21007180#comment_21007262:
                          Кстати, в статье я привел пример такой же потенциальной проблемы, которая возникает из-за отписки (в случае если нет блокировки закрытия окна). Но вы это игнорируете, даже не пытаетесь возразить что-то конкретное. Это ли на фанатизм?


                          Не вы ли говорили, что преждевременная оптимизация это зло? К тому же, чтобы снизить нагрузку на бекенд, есть масса хороших способов шарить потоки, организовывать локальный кэш, инвалидировать его и так далее. Но я не считаю, что блокировать пользовательский интерфейс является хорошей практикой, хотя бы потому что пользователи не любят ждать, все пользователи хотят Optimistic UI.

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


                          1. Andchir Автор
                            14.12.2019 00:10

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

                            Не вы ли говорили, что преждевременная оптимизация это зло?
                            Это был только пример того, что отписываться нужно не всегда. Я там несколько раз сделал оговорку, что не предлагаю так делать везде и всегда и «на всякий случай».

                            Что вы пристали к этим отпискам? Разве статья называется «не отписывайтесь»? Нет. Не это главное.


                            1. thekiba
                              14.12.2019 00:19

                              А дальше не надо читать? Не вырывайте кусочек и контекста.


                              Можете показать, что именно мне необходимо прочитать? Я действительно не вижу, что вы имеете в виду.

                              Это был только пример того, что отписываться нужно не всегда. Я там несколько раз сделал оговорку, что не предлагаю так делать везде и всегда и «на всякий случай».


                              Вы правда считаете, что блокировать UI — это действительно хорошая идея и предлагаете делать это всем?


                              1. Andchir Автор
                                14.12.2019 00:25

                                Можете показать, что именно мне необходимо прочитать? Я действительно не вижу, что вы имеете в виду.
                                Со слов «Т.е. человек перехватил конечный стрим...». Вы изменили логику приложения, поэтому появилась проблема. Это Вы сделали не для какого-то полезного действия внутри программы, а просто для вывода строки в консоль. Тем самым ввели читателя в заблуждение. Существует тысяча способов изменить логику приложения и нарушить его работу. Ещё в районе слов (выше) «Выходит, что „учитель“ был не прав, когда говорил...».

                                Вы правда считаете… и предлагаете...
                                Это феноминально. Я пишу «я не предлагаю...», а он мне совсем обратный смысл. Я считаю, что в некоторых случая это вполне нормальное решение. А если вы будете и это за меня решать, то может будете за меня писать код?

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


                                1. thekiba
                                  14.12.2019 00:32

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


                                  Как минимум некрасиво с вашей стороны вот так поступать. Сначала обращаетесь ко мне публично «человек», обвиняете в том, что я не делал, а потом уходите от ответов на вопросы.

                                  Хоть бы извинились сперва, но да ладно, я обид не держу) Если будет желание пообщаться и разобраться, то всегда можете мне написать в телеграмм.


                      1. Andchir Автор
                        13.12.2019 22:38

                        И, простите, но вот как раз из-за подобных решений и была написана та статья с примерами: habr.com/ru/post/479732/#comment_21004844
                        Это только для проверки. Там, откуда я взял этот код (мой код), даже есть комментарий:
                        github.com/andchir/test-angular-app/blob/master/my-app/src/app/components/good-modal/good-modal.component.ts#L31


                      1. Andchir Автор
                        13.12.2019 22:50

                        Кстати, в статье я привел пример такой же потенциальной проблемы, которая возникает из-за отписки (в случае если нет блокировки закрытия окна). Но вы это игнорируете, даже не пытаетесь возразить что-то конкретное. Это ли на фанатизм?
                        Но в отличии от вас, я никого не запугиваю, давая ложную (непроверенную) информацию, я ничего не утаиваю и не говорю, что моему примеру нужно следовать всегда.


              1. Andchir Автор
                13.12.2019 19:53

                Я да, я не мою руки, если знаю, что они чистые (не ходил на улицу, не трогал ничего грязного, уже мыл 5 минут назад и т.д.).


            1. ganqqwerty
              13.12.2019 19:22

              Это не преждевременная оптимизация, это нормальная минимизация рисков. Разработчик забудет. Разработчик всегда что-то забывает — если вы сами не такой, то в вашей команде точно кто-то такой. Напишите unsubscribe, минусы от этого гораздо слабее, чем потенциальный риск от разгребания кода в поисках зависшего потока.


              1. Andchir Автор
                14.12.2019 08:46

                Это не преждевременная оптимизация, это нормальная минимизация рисков.
                Согласен, отписка это не преждевременная оптимизация. Я писал не об этом. Читайте обновление #3 внизу статьи.


            1. limitofzero
              14.12.2019 20:24
              +1

              А вы не подменяете понятий? Причем тут «преждевременная оптимизация» и безопасный код? С кодом работать гораздо проще, когда ты знаешь, что при модификации потока у тебя точно будет происходить отписка, а не сидеть и не изучать исходники либ/сорсов из которых этот поток тянется. Что проще, потратить минуту на написание одной строчки с unsubscribe/switchMap/takeUntil, чем каждый раз смотреть исходники или вставлять ненужный «отладочный» код и проверять его работу?


              1. Andchir Автор
                14.12.2019 21:22
                -1

                «Причем тут «преждевременная оптимизация» и безопасный код?»
                Не вижу смысла повторять, в статье есть даже дополнение по этой теме. Я не называл отписку преждевременной оптимизацией.
                Что проще, потратить минуту на написание одной строчки с unsubscribe/switchMap/takeUntil, чем каждый раз смотреть исходники или вставлять ненужный «отладочный» код и проверять его работу?
                На самом деле там всё просто, это же просто JavaScript функции, которые могут быть синхронными и асинхронными. Отписываться от синхронных бессмысленно. Вы же не вызываете «removeEventListener» после того как отфильтровали массив? Там точно такая же логика. Загляните в исходники хотя бы один раз и всё станет понятно.
                Это из документации (что происходит при отписке в бесконечном потоке — для понятности переписано):
                image
                А это из исходников, как создается подписка на событие HTML-элемента (сразу создается функция отписки):
                image

                В случае асинхронных, конкретно запрос в HttpClient можно в некоторых случаях считать исключением из правил, потому, что поток завершается автоматически. Мне никакие проверки не нужны, проверку я привел для сомневающихся.


                1. limitofzero
                  14.12.2019 21:46

                  Спасибо, но исходники я видел. Мой посыл вы однако не поняли. Зачем мне вообще в них заглядывать, если я использую простой контракт: «подписка = отписка»? Это первое.
                  Ну и конечно же второе: сегодня у них одна реализация, а завтра они ее меняют. Какой код более устойчив к изменениям реализации того или иного Observable?
                  Ну и конечно же третий довод: От отписки еще ни одной утечки/нежелательных последствий в коде не было, а вот от их избегания…

                  И чисто от себя: я всегда делаю отписку, будь то EventListener, паттерн Observable или его реализация ввиде Rxjs. Я даже в сервисах всегда ставлю отписку, потому что сегодня он у меня синглтон, а завтра я начну провайдить его в компоненты и он уже не синглтон.


                  1. Andchir Автор
                    14.12.2019 21:50
                    -2

                    Читайте обновление #3 внизу статьи. Мне добавить к этому нечего. А если вы с чем-то не согласны, то приводите конкретные цитаты из текста и свои аргументы против.


                    1. limitofzero
                      14.12.2019 21:55

                      От одной отписки, перегруженным код не станет(а все подписки внутри сервиса или компонента всегда можно свести к одной строчке отписки). А вот то что он станет более безопасным и менее подверженным ошибкам — это более серьезный довод в эту сторону.


                      1. Andchir Автор
                        14.12.2019 23:23
                        -2

                        А вот то что он станет более безопасным и менее подверженным ошибкам — это более серьезный довод в эту сторону.
                        Это вы пишите, основываясь на личной практике, или это теория? У меня тоже есть не малый опыт. Мне сложно представить, чтобы я пришел в какой-то проект и начал бездумно, не глядя, втыкать какой-то свой код. Обычно в команде пишутся юнит-тесты. Они уберегут от ошибок, которые может сделать другой разработчик в вашем коде. Это и есть своего рода стандарт. А то, что говорите вы, стандартом не является. Приходя в какой-то проект, вы уверены, что разработчик, который делал проект до вас, читал те же Best-practice что и вы? Я думаю это маловероятно. А когда вы видите юнит-тесты, вряд ли Вы будете фантазировать. Прошу ответить на вопрос в первом предложении этого комментария. Только честно.


                        1. limitofzero
                          14.12.2019 23:53
                          +1

                          Юнит тесты — видимо у вас очень удачный подбор компаний, потому что я за свою практику встречал всего пару компаний, где фронтенд приложения покрывают юнит тестами.
                          А по поводу бездумности — как много новичков вы знаете, которые начинают приступать к проекту только после чтения всей документации и просмотра ее исходников? Лично я ни одного такого человека не встречал. Доки обычно смотрятся вскользь, а возвращаются к ним в моменты, когда что-то идет не так, или спустя время, когда менеджер не стоит над душой с новым багом или фичей.


    1. Xuxicheta
      13.12.2019 17:21
      +1

      именно так. В контекте ангуляра есть много вещей, где разработчики гарантируют что поток будет завершен при дестрое компонента. А вдруг это не так, вдруг оно изменится, вдруг где-то ошибка?
      Я видел проекты которые вели себя весьма странно, где разработчики особо не заморачивались. Рендер кусков убитых компонентов например.


  1. Andchir Автор
    15.12.2019 13:09

    В этом комментарии я приведу два ответа от авторитетных людей в области Angular и RxJS. Выводы делайте сами. Лично мой вывод — RxJS решает одни проблемы и создает новые. Вмеcто того, чтобы расслабиться, ты постоянно в напряжении. У этой библиотеки (методики) есть недостатки (о которых все молчат). Первый человек заботится об облегчении жизни разработчиков, а второй нет. Но второй более авторитетный.

    Первый ответ от одного из активных участников команды разработки Angular и NgRx Роба Уормолда:
    Вопрос:

    Ответ:


    Мой перевод:

    — В хангаут-разговоре на 27:40 ты говоришь о самоочистке Observables, это происходит всегда?
    Необходимо ли нам отписываться, например, в ngOnDestroy в качестве хорошей практики?

    — Не видел этот вопрос. Если это последовательность [операция — прим. переводчика] с единичным значением (как http запрос), ручная очистка не обязательна (имея ввиду, что ты подписываешься вручную в контроллере). Если это бесконечная последовательность, ты должен отписаться, что и делает для тебя async pipe.


    Второй ответ от разработчика RxJS Бена Леша:

    Мой перевод:
    — Привет, Бен. Можешь ты мне сказать ВСЕГДА ли мне нужно отписываться от rxjs потоков?
    Я не могу найти это в твоих статьях или публичных видео. Спасибо.

    — Если это синхронные, или просто несколько значений и это заканчивается (и тебя не волнуют ресурсы), ты можешь забить (если ты лентяй) и не отписываться.

    — Но это лучшая практика отписываться.

    — Да.


    В телеграм чате я обещал человеку под ником Reactive Fox, что если он найдет рекомендацию исполнения контракта «отписался — отпишись» от самого разработчика RxJS, то я принесу ему публичные извинения.
    Прошу прощения. Я был не прав, когда говорил, что такой рекомендации в RxJs Нет.
    Только плохо, что такой рекомендации нет в документации RxJS.


    1. Andchir Автор
      15.12.2019 16:23

      «отписался — отпишись»
      Ошибка. Правильно: «Подписался — отпишись».