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

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

Случай из реального мира

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

Итак, давайте посмотрим, как выглядит директива:

Как видите, эта директива предоставляет несколько настраиваемых входных данных, таких как events (указывает, какие события следует прослушивать) и properties (позволяет нам прикреплять некоторые дополнительные свойства к зарегистрированным событиям). properties не требуется для демонстрации проблемы, но я обещал реальный пример, не так ли?

Теперь давайте рассмотрим доступные методы. ngOnChanges использует два метода:

  • registerEvents регистрируется для всех событий в данном списке и возвращает наблюдаемое, которое испускается, когда происходит одно из них.
  • logEvent регистрирует указанный тип и свойства события с помощью службы пользовательской аналитики.

Наши проницательные глаза наверняка заметили, что я использовал оператор untilDestroyed (от Netanel Basal) для отмены подписки на зарегистрированные события, когда наш директива будет уничтожена, но достаточно ли она хороша?

Эта проблема

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

Давайте пройдемся по потоку директив на следующем примере использования и посмотрим на результат скрытой проблемы в текущей реализации OnChanges:

  1. Мы использовали директиву где-то в приложении с событиями, которые хотим регистрировать.
  2. Первоначально isEditMode === false, поэтому вход events теперь ['focus'].
  3. Вызывается OnChanges, и наша директива регистрируется в событии ‘focus’.
  4. Пользователь сосредоточен на нашем text-editor компоненте, а событие ‘focus’ регистрируется в провайдере аналитики.
  5. Пользователь нажимает какую-нибудь кнопку редактирования, затем (editModeChange) выдает и теперь isEditMode === true.
  6. Вход events изменился на [‘contextmenu’, ‘focus’], OnChanges вызывается снова, и директива перерегистрирована в новый список событий.
  7. Пользователь сосредоточен на компоненте text-editor, и событие ‘focus’ снова регистрируется.

Теперь, если мы зарегистрируем зарегистрированные выбросы подписки на события, вы увидите, что событие ‘focus’ регистрируется три раза, тогда как мы ожидали, что оно будет зарегистрировано только дважды. Это происходит потому, что мы не отменили подписку на первые наблюдаемые зарегистрированные события до регистрации в новом списке входящих событий.

Итак, как мы можем это исправить? Давайте откроем наш набор инструментов, засучим рукава и обновим эту директиву!

Решение №1: отказаться от подписки

Да, это так просто! Отмените подписку на предыдущую подписку перед тем, как подписаться на новую. Точно так же, как вы отказываетесь от подписки на наблюдаемые за момент до уничтожения директивы, вам нужно сделать то же самое в этом случае, чтобы отбросить предыдущую подписку.

Посмотрите, как это реализовано в нашей директиве:

Решение №2: Тема + takeUntil

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

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

Решение № 3: Rehooktive + takeUntil

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

И вот как легко это может работать в нашей директиве:

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

Другой вариант полного реактивного решения - это использование оператора switchMap и сопоставление любого выброса OnChanges, включая изменение ввода событий на новые зарегистрированные наблюдаемые события внутри конструктора директивы. Вот результат:



Резюме

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

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

Спасибо за прочтение!