rxjs - как вы можете создать другой наблюдаемый объект из наблюдаемого, но игнорировать его вывод в родительском наблюдаемом?

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

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

import { fromEvent, from } from "rxjs";
import { mapTo, switchMap, tap, scan  } from "rxjs/operators";

const buttonA = document.getElementById("a");
const buttonB = document.getElementById("b");

const textA = document.querySelector('#texta');
const textB = document.querySelector('#textb');

fromEvent(buttonA, 'click').pipe(
  // this toggles active or not.
  scan((active) => !active, false),
  switchMap(active => {

    if (active) {
      const buttonBClicks$ = fromEvent(buttonB, 'click');
 
      // here we can observe button b clicks, when button a is toggled on.
      return buttonBClicks$.pipe(
        // count the sum of button b clicks since button a was toggled on.
        scan((count) => count+1, 0),
        tap(buttonBCount  => {
          textB.value = `button b count ${buttonBCount}`;
        }),
        // ignore the value of the button b count for the final observable output. 
        mapTo(active)
      )
    } else {
      textB.value = ``;

      return from([active]);
    }
   
  })
).subscribe({
  next: buttonActive => {
    textA.value = `Button a active: ${buttonActive}`
  }
});

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

Есть ли лучшие способы сделать это?


person Oved D    schedule 04.02.2021    source источник


Ответы (2)


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

Если нет, вы можете сделать все это как побочный эффект следующим образом:

fromEvent(buttonA, 'click').pipe(
  scan((active) => !active, false),
  tap(active => { if(active) {
    fromEvent(buttonB, 'click').pipe(
      scan(count => count+1, 0),
      tap(buttonBCount  => {
        textB.value = `button b count ${buttonBCount}`;
      })
    ).subscribe()   
  }})
).subscribe({
  next: buttonActive => {
    textA.value = `Button a active: ${buttonActive}`
  }
});

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

const trackActiveFromButton$ = fromEvent(buttonA, 'click').pipe(
  scan((active) => !active, false),
  shareReplay(1)
);

trackActiveFromButton$.subscribe({
  next: buttonActive => {
    textA.value = `Button a active: ${buttonActive}`
  }
});

trackActiveFromButton$.pipe(
  switchMap(active => active ?
    fromEvent(buttonB, 'click').pipe(
      scan(count => count+1, 0),
      tap(buttonBCount  => {
        textB.value = `button b count ${buttonBCount}`;
      })
    ) :
    EMPTY
  )
).subscribe();
person Mrk Sef    schedule 04.02.2021

Есть ли лучшие способы сделать это?

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

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

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

Если мы объявляем поток для представления состояния isActive и подписываемся на него для обновления textA, а также определяем поток counter для представления количества кликов, произошедших во время isActive = true, используя это значение для обновления textB, я думаю, это упрощает отслеживание того, что продолжается:

const clicksA$ = fromEvent(buttonA, 'click');
const clicksB$ = fromEvent(buttonB, 'click');

const isActive$ = clicksA$.pipe(
  scan(active => !active, false),
  startWith(false)
);

const counterB$ = combineLatest([isActive$, clicksB$]).pipe(
  scan((count, [isActive]) => isActive ? count + 1 : -1, 0)
);
   
counterB$.subscribe(
  count => textB.value = count === -1 ? '' :`button b count ${count}`
);

isActive$.subscribe(
    isActive => textA.value = `Button a active: ${isActive}`
);

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

  • isActive происходит от clicksA
  • counterB происходит от clicksB & isActive

Вот рабочий StackBlitz

Также:

внешняя наблюдаемая получает значение только после нажатия кнопки

Эту проблему можно решить, используя startWith() для выдачи значения по умолчанию.

person BizzyBob    schedule 04.02.2021