Other operator in calculation chain than combineLatest to avoid redundant calculations

自古美人都是妖i 提交于 2020-01-04 05:21:22

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!