RxJS: MergeMap with Preserving Input order

二次信任 提交于 2019-12-30 11:21:51

问题


Requirement:

urls = [url1, url2, url3]

Fire all 3 urls parallely and paint the Dom in the sequnce of the urls list

 ex: Finished order of urls = [url3, url1, url2]
     when url1 finishes Immediately render the DOM, without waiting for url2
     If url2, url3 finishes before url1, then store url2, url3 and paint the DOM after url1 arrives
     Paint the DOM with order [url1, url2, url3]

My Work using promises:

// Fired all 3 urls at the same time
p1 = fetch(url1)
p2 = fetch(url2)
p3 = fetch(url3)

p1.then(updateDom)
  .then(() => p2)
  .then(updateDom)
  .then(() => p3)
  .then(updateDom)

I wanted to do the same thing in Observables.

from(urls)
  .pipe(
      mergeMap(x => fetch(x))
  )

To fire them parallely I used merge map, but how can I order the sequence of the results?


回答1:


I couldn't find anything that preserves the order so I came up with something a bit convoluted.

const { concat, of, BehaviorSubject, Subject } = rxjs;
const { delay, filter } = rxjs.operators;

const parallelExecute = (...obs$) => {
  const subjects = obs$.map(o$ => {
    const subject$ = new BehaviorSubject();
    const sub = o$.subscribe(o => { subject$.next(o); });
    return { sub: sub, obs$: subject$.pipe(filter(val => val)) };
  });
  const subject$ = new Subject();
  sub(0);
  function sub(index) {
    const current = subjects[index];
    current.obs$.subscribe(c => {
      subject$.next(c);
      current.obs$.complete();
      current.sub.unsubscribe();
      if (index < subjects.length -1) {
        sub(index + 1);
      } else {
        subject$.complete();
      }
    });
  }
  return subject$;
}


parallelExecute(
  of(1).pipe(delay(3000)),
  of(2).pipe(delay(2000)),
  of(3).pipe(delay(1000))
).subscribe(result => { console.log(result); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>



回答2:


The best way to preserve order with async tasks like this is with concatMap.

The problem is that if we apply this alone, we lose the parallelisation. If we were to do something like this:

from(urls)
  .pipe(
      concatMap(x => fetch(x))
  )

the second request is not fired until the first is complete.

We can get around this by separating out the map into its own operator:

from(urls)
  .pipe(
      map(x => fetch(x)),
      concatMap(x => x)
  )

The request will all be fired at the same time, and the results will be emitted in request order.

See Adrian's example adapted to use this approach below:

const { from } = rxjs;
const { concatMap, map } = rxjs.operators;

function delayPromise(val, delay) {
  return new Promise(res => setTimeout(() => res(val), delay));
}

var delay = 3;


from([1, 2, 3]).pipe(
  map(x => delayPromise(x, delay-- * 1000)),
  concatMap(x => x)
).subscribe(result => { console.log(result); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>



回答3:


You can form a sequence with fetch and paint then forkJoin/Promise.all them

p1 = fetch(url1)
p2 = fetch(url2)
p3 = fetch(url3)

forkJoin(
from(p1).pipe(tap(_=>paint dom...))
from(p1).pipe(tap(_=>paint dom...))
from(p1).pipe(tap(_=>paint dom...))
).subscribe()


来源:https://stackoverflow.com/questions/55035163/rxjs-mergemap-with-preserving-input-order

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