Использование оператора takeUntil для автоматического отказа от подписки на наблюдаемое - это механизм, описанный в статье Бена Леша Не отказываться от подписки.

Это также основа общепринятого шаблона для отказа от подписки при разрушении компонента Angular.

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

Давайте посмотрим на последовательность проблем и причину утечек.

Какая последовательность проблем?

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

Например, такое использование combineLatest приведет к утечке подписки на b:

И это использование switchMap также приведет к утечке подписки на b:

Почему течет?

Причина утечки становится более очевидной, когда устаревший оператор combineLatest заменяется статической фабричной функцией combineLatest, например:

Когда notifier излучает, наблюдаемое, возвращаемое оператором takeUntil, завершается, автоматически отменяя подписку всех подписчиков.

Однако подписчик на c не подписан на наблюдаемый объект, возвращаемый takeUntil - он подписан на наблюдаемый объект, возвращаемый combineLatest, поэтому он не отменяется автоматически после завершения takeUntil наблюдаемого.

Подписчик на c останется подписанным до тех пор, пока не будут выполнены все наблюдаемые, переданные в combinedLast. Таким образом, если b не будет завершено до отправки notifier, подписка на b будет протекать.

Чтобы избежать этой проблемы, по общему правилу takeUntil должен быть последним оператором в последовательности:

При такой организации, когда notifier излучает, подписчик на c будет автоматически отказываться от подписки - поскольку наблюдаемое, возвращаемое takeUntil, будет завершено, - и реализация takeUntil отменит подписку на наблюдаемое, возвращенное combineLatest, которое, в свою очередь, отменит подписку как на a, так и на b.

Использование TSLint, чтобы избежать проблемы

Если вы используете механизм takeUntil для косвенной отмены подписки, вы можете убедиться, что это всегда последний оператор, передаваемый в канал, включив правило rxjs-no-unsafe-takeuntil, которое я добавил в пакет rxjs-tslint-rules.

Обновление

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

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

Когда наблюдаемое завершается из-за takeUntil, такие операторы, как count и toArray, будут выдавать значение, только если они помещены после оператора takeUntil.

Кроме того, есть еще один оператор, который следует разместить после takeUntil: оператор shareReplay.

Текущая реализация shareReplay имеет ошибку / особенность: она никогда не откажется от подписки на свой источник. Он будет оставаться подписанным до тех пор, пока его источник не выдаст ошибку или не завершится - для получения дополнительной информации см. Этот PR - поэтому размещение takeUntil после shareReplay будет неэффективным.

Упомянутое выше правило TSLint знает об этих исключениях из общего правила и не приводит к неоправданным сбоям.

В RxJS версии 6.4.0 shareReplay был изменен, так что его поведение при подсчете ссылок можно было указать с помощью параметра config. Если указан подсчет ссылок, shareReplay можно безопасно разместить перед takeUntil.

Для получения дополнительной информации см. Эту статью.

Этот пост также опубликован в моем личном блоге: ncjamieson.com.