问题
I've successfully migrated a larger Excel calculation to JS using RxJS. Any input data is represented as an Observable and subsequent calculations are implemented with .map
and .combineLatest
when any Excel formula uses more than one input.
This works great except for one problem. Here is a simplified example:
Three inputs (a$=1
, b$=2
, c$=3
) are used in two different calculations ($ab = $a+$b = 3
in the first, $bc = $b+$c = 5
in the second) as intermediate steps to calculate the final result $abbc = $ab + $bc = 8
.
When $b is now updated/emitting a new input value (e.g. 4
), $abbc is calculated twice - first when $ab is updated (resulting in the wrong result $abbc=10
) and again when $bc is updated, resulting in the correct result 12
.
While the final result is correct, the intermediate calculation is both wrong and redundant. Is there any way to only execute the last calculation in case $b
gets updated - while still also updating the calculation when a$
or c$
is updated (which would rule out the zip operator). I understand that this example can obviously be simplified to leave out the intermediate steps and calculated $abbc
directly from $a
, $b
and $c
- but in the real live example this is not possible/feasible.
Here's the running example in JSbin: https://jsbin.com/pipiyodixa/edit?js,console
回答1:
The problem here is that RxJS's behavior is correct by its design.
It really should first update b
=> ab
=> abbc
and then bc
=> abbc
. It processed values in streams.
What you want is to process values in "layers".a
, b
, c
, then ac
, bc
and after that calculate final value abbc
.
The only way I can think of is to make use of JavaScript execution context and a trick with setTimeout(() => {}, 0)
. This way you don't schedule any timeout (in fact the real timeout might be > 0) and just run the closure in another execution context after JavaScript finishes executing the current one.
The bad thing is that to avoid re-emitting values multiple times you need to cache even more (because of merge()
):
var b2$ = b$.cache(1);
var ab$ = a$
.combineLatest(b2$, (a, b) => a + b)
.do(x => console.log('$a + $b = ' + x))
.cache(1);
var bc$ = b2$
.combineLatest(c$, (b, c) => b + c)
.do(x => console.log('$b + $c = ' + x))
.cache(1);
var abbc$ = new Rx.Observable.merge(ab$, bc$)
.auditTime(0)
.withLatestFrom(ab$, bc$, (_, ab, bc) => ab + bc)
.do(x => console.log('$ab + $bc = ' + x));
console.log("Initial subscription:")
abbc$.subscribe();
b$.next(4);
The auditTime() operator is the most important thing here. It triggers withLatestFrom()
to update it's value while when it's triggered for first time by ab$
or bc$
it ignores all consecutive emits until the end of this closure execution (that's the setTimeout()
trick).
See live demo: https://jsbin.com/zoferid/edit?js,console
Also, if you add a$.next(5);
the final calculation is executed just once (which might be both good or bad :)).
I don't know if this solves your problem because as I said RxJS works this way. I haven't tested it on any more complicated example so maybe this is not the way you can use at the end.
Note that cache()
operator was removed in RC.1 and there's no replacement for now: https://github.com/ReactiveX/rxjs/blob/master/CHANGELOG.md
回答2:
Based on @martin's (correct) answer, I created an rxjs operator that does the combineLatestDelayed:
Rx.Observable.prototype.combineLatestDelayed = function(b$, cb) {
var a2$ = this.cache(1);
var b2$ = b$.cache(1);
return a2$
.merge(b2$)
.auditTime(0)
.withLatestFrom(a2$, b2$, (_, a, b) => cb(a,b));
}
This way, I only need to replace the original .combineLatest
calls with .combineLatestDelayed
:
var a$ = new Rx.BehaviorSubject(1).do(x => console.log("Emitting $a=" + x));
var b$ = new Rx.BehaviorSubject(2).do(x => console.log("Emitting $b=" + x)); var b1$ = b$.cache(1);
var c$ = new Rx.BehaviorSubject(3).do(x => console.log("Emitting $c=" + x));
var ab$ = a$
.combineLatestDelayed(b1$, (a, b) => a + b)
.do(x => console.log('ab$: $a + $b = ' + x))
var bc$ = b1$
.combineLatestDelayed(c$, (b, c) => b + c)
.do(x => console.log('bc$: $b + $c = ' + x))
var abbc$ = ab$
.combineLatestDelayed(bc$, (ab, bc) => ab + bc)
.do(x => console.log('$abbc: $ab + $bc = ' + x));
console.log("Initial subscription:")
abbc$.subscribe();
setTimeout(() => b$.next(4), 100);
Full JS Bin here
来源:https://stackoverflow.com/questions/40455579/other-operator-in-calculation-chain-than-combinelatest-to-avoid-redundant-calcul