如何从一个可观察对象创建另一个可观测对象,却忽略其在父可观测对象中的输出



我有一种情况,我有一个可观察项,对于每个发出的项,我想创建另一个可观测项,但忽略该可观测项的值,而是返回第一个可观测的结果
例如,如果我单击一个按钮,我想跟踪另一个按钮中发生的事情,只有当第一个按钮打开时。

我现在可以通过破解来做到这一点,方法是获取可观察到的子对象的输出,并将其与父对象的值管道连接到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的使用似乎很古怪。

有更好的方法吗?

听起来你根本不想让内部可观察对象成为过程的一部分。你在等它吗?

如果没有,你可以把它全部作为副作用,如下所示:

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}`
}
});

嵌套订阅被认为是糟糕的voodoo,所以你可以这样重构,以保持你的conserns分离更加明显:

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();

有更好的方法吗?

根据您的口味,下面的可能会更好。在我看来,您的示例代码变得有点混乱,因为您有一个可观察的对象,它试图做太多的事情。副作用在某种程度上与流行为逻辑混合在一起。

使用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
  • CCD_ 11源自CCD_;isActive

这是一个正在工作的StackBlitz

还有:

外部可观察对象只有在点击按钮后才会收到值

这可以使用startWith()发出默认值来解决。

最新更新