Когда я должен создать новую подписку для определенного побочного эффекта?

10

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

Информация о настройке

  • Все примеры в TypeScript
  • Чтобы лучше сосредоточиться на вопросе, не использовать жизненные циклы / конструкторы для подписок и держать в рамках не связанными
    • Представьте себе: подписки добавляются в init конструктора / жизненного цикла
    • Представьте себе: отмена подписки осуществляется в жизненном цикле уничтожения

Что такое побочный эффект (угловой образец)

  • Обновление / ввод в пользовательском интерфейсе (например value$ | async)
  • Выход / восходящий поток компонента (например @Output event = event$)
  • Взаимодействие между различными службами в разных иерархиях

Примерный вариант использования:

  • Две функции: foo: () => void; bar: (arg: any) => void
  • Две исходные наблюдаемые: http$: Observable<any>; click$: Observable<void>
  • fooвызывается после того, как http$выпустил и не нуждается в значении
  • barвызывается после click$эмиссии, но нуждается в текущем значенииhttp$

Случай: создать подписку для каждого конкретного побочного эффекта

const foo$ = http$.pipe(
  mapTo(void 0)
);

const bar$ = http$.pipe(
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue)
  )
);

foo$.subscribe(foo);
bar$.subscribe(bar);

Дело: Минимизировать подписки в целом

http$.pipe(
  tap(() => foo()),
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue )
  )
).subscribe(bar);

Мое собственное мнение вкратце

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

Джонатан Стеллваг
источник

Ответы:

6

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

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

Пример исключения - повторное использование наблюдаемых в одном шаблоне

Глядя на ваш первый пример:

// Component:

this.value$ = this.store$.pipe(select(selectValue));

// Template:

<div>{{value$ | async}}</div>

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

// It works, but don't do this...

<ul *ngIf="value$ | async">
    <li *ngFor="let val of value$ | async">{{val}}</li>
</ul>

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

// Component

valueSub: Subscription;
value: number[];

ngOnInit() {
    this.valueSub = this.store$.pipe(select(selectValue)).subscribe(response => this.value = response);
}

ngOnDestroy() {
    this.valueSub.unsubscribe();
}

// Template

<ul *ngIf="value">
    <li *ngFor="let val of value">{{val}}</li>
</ul>

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

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

Если две или более наблюдаемых используются только вместе, соответствующие операторы RxJS должны использоваться для объединения их в одну подписку.

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

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

Относительно отписок:

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

Личные предпочтения / многословие против краткости:

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

Мэтт Сондерс
источник
Сначала спасибо за подробный ответ! Что касается # 1: с моей точки зрения, асинхронный канал также является побочным эффектом подписок, просто он маскируется внутри директивы. Что касается # 2, можете ли вы добавить пример кода, я не понимаю, о чем вы мне говорите. Что касается отмены подписки: у меня никогда не было этого, чтобы подписки нужно было отписывать в любом другом месте, кроме ngOnDestroy. First () на самом деле не управляет отпиской для вас по умолчанию: 0 emits = подписка открыта, хотя компонент уничтожен.
Джонатан Стеллваг
1
Пункт 2 на самом деле просто рассматривал роль каждой наблюдаемой при принятии решения о том, следует ли также устанавливать подписку, а не автоматически следовать за каждой наблюдаемой с подпиской. На этом этапе я бы посоветовал взглянуть на medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 , где написано: «Наличие слишком большого количества объектов подписки - это признак того, что вы управляете своими подписками настоятельно, а не пользуетесь преимуществами силы Rx. "
Мэтт Сондерс
1
Благодарим Вас за @JonathanStellwag. Спасибо также за обратные вызовы info RE - в своих приложениях вы явно отписываетесь от каждой подписки (даже если используется, например, first ()), чтобы предотвратить это?
Мэтт Сондерс
1
Да. В текущем проекте мы свели к минимуму около 30% всех узлов, просто отказавшись от подписки.
Джонатан Стеллваг
2
Я предпочитаю отписываться takeUntilв сочетании с функцией, которая вызывается из ngOnDestroy. Это один лайнер , который добавляет это к трубе: takeUntil(componentDestroyed(this)). stackoverflow.com/a/60223749/5367916
Курт Гамильтон
2

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

 const obs1$ = src1$.pipe(tap(effect1))
 const obs2$ = src2$pipe(tap(effect2))
 merge(obs1$, obs2$).subscribe()

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

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

Я бы сказал, что ваши наблюдаемые должны быть логически составлены, а не загрязнены или запутаны во имя сокращения подписок. Должен ли эффект foo логически сочетаться с эффектом бара? Нужно ли одно другому? Могу ли я когда-нибудь не захотеть запускать foo, когда http $ выбрасывает? Я создаю ненужную связь между несвязанными функциями? Это все причины, чтобы не помещать их в один поток.

Это все даже не учитывая обработку ошибок, которой проще управлять с помощью нескольких подписок IMO

bryan60
источник
Спасибо за ваш ответ. Мне жаль, что я могу принять только один ответ. Ваш ответ такой же, как и у @Matt Saunders. Это просто другая точка зрения. Благодаря усилиям Мэтта я дал ему согласие. Я надеюсь, что вы можете простить меня :)
Джонатан Stellwag