Come fare in modo che una sequenza osservabile attenda il completamento di un'altra prima di emetterla?


93

Di 'che ho un Observable, in questo modo:

var one = someObservable.take(1);

one.subscribe(function(){ /* do something */ });

Quindi, ho un secondo Observable:

var two = someOtherObservable.take(1);

Ora, voglio subscribe()a two, ma voglio fare in modo che oneha completato prima che il twosottoscrittore è sparato.

Che tipo di metodo di buffering posso usare twoper far aspettare il secondo che il primo sia completato?

Suppongo che sto cercando di mettere in pausa twofino al onecompletamento.


1
Credo che la risposta a questa domanda sia il metodo .exhaustMap (), tuttavia non pretenderei di sapere come implementarlo - descrizione completa qui: blog.angular-university.io/rxjs-higher-order-mapping
Peter Nixey

Risposte:


56

Un paio di modi in cui riesco a pensare

import {take, publish} from 'rxjs/operators'
import {concat} from 'rxjs'

//Method one

var one = someObservable.pipe(take(1));
var two = someOtherObservable.pipe(take(1));
concat(one, two).subscribe(function() {/*do something */});

//Method two, if they need to be separate for some reason
var one = someObservable.pipe(take(1));
var two = someOtherObservable.pipe(take(1), publish());
two.subscribe(function(){/*do something */});
one.subscribe(function(){/*do something */}, null, two.connect.bind(two));

1
Ho finito per usare pausee resumeinvece di publishe connect, ma l'esempio due è essenzialmente il percorso che ho seguito.
Stephen

1
Questo metodo farà sempre onerisolvere la prima osservabile ( ) prima della seconda ( two) all'interno della funzione subscribe ()?
John

Perché non utilizzare Observable.forkJoin()? Vedi questo link learnrxjs.io/operators/combination/forkjoin.html
mspasiuk

18
@mspasiuk per i requisiti degli OP, volevano che il secondo si abbonasse solo dopo che il primo era stato completato. forkJoinsi iscrive contemporaneamente.
paulpdaniels

18

Se vuoi assicurarti che l'ordine di esecuzione sia mantenuto, puoi usare flatMap come il seguente esempio

const first = Rx.Observable.of(1).delay(1000).do(i => console.log(i));
const second = Rx.Observable.of(11).delay(500).do(i => console.log(i));
const third = Rx.Observable.of(111).do(i => console.log(i));

first
  .flatMap(() => second)
  .flatMap(() => third)
  .subscribe(()=> console.log('finished'));

Il risultato sarebbe:

"1"
"11"
"111"
"finished"

16

skipUntil () con last ()

skipUntil: ignora gli elementi emessi fino a quando un altro osservabile non ha emesso

last: emette l'ultimo valore da una sequenza (cioè aspetta fino al suo completamento quindi emette)

Si noti che qualsiasi cosa emessa dall'osservabile passato a skipUntilannullerà il salto, motivo per cui dobbiamo aggiungere last()- per attendere il completamento dello stream.

main$.skipUntil(sequence2$.pipe(last()))

Ufficiale: https://rxjs-dev.firebaseapp.com/api/operators/skipUntil


Possibile problema: si noti che last()da solo si verificherà un errore se non viene emesso nulla. L' last()operatore ha un defaultparametro, ma solo se usato insieme a un predicato. Penso che se questa situazione è un problema per te (se sequence2$può essere completata senza emettere), allora uno di questi dovrebbe funzionare (attualmente non testato):

main$.skipUntil(sequence2$.pipe(defaultIfEmpty(undefined), last()))
main$.skipUntil(sequence2$.pipe(last(), catchError(() => of(undefined))

Nota che undefinedè un elemento valido da emettere, ma potrebbe effettivamente essere qualsiasi valore. Notare inoltre che questo è il tubo attaccato sequence2$e non il main$tubo.


Demo molto goffo: angular-vgznak.stackblitz.io Devi fare clic per aprire il vassoio della console
Simon_Weaver

La tua sintassi è sbagliata. skipUntil non può essere collegato direttamente a un osservabile, altrimenti verrà visualizzato il seguente errore: "Proprietà" skipUntil "non esistente nel tipo" Observable <any> "." Devi prima eseguirlo tramite .pipe ()
London804

Sì, questa è una vecchia risposta prima che fosse richiesta la pipa. Grazie per averlo menzionato. Lo aggiornerei ora ma sono sul mio telefono. Sentiti libero di modificare la risposta.
Simon_Weaver

13

Ecco un'altra possibilità che sfrutta il selettore dei risultati di switchMap

var one$ = someObservable.take(1);
var two$ = someOtherObservable.take(1);
two$.switchMap(
    /** Wait for first Observable */
    () => one$,
    /** Only return the value we're actually interested in */
    (value2, value1) => value2
  )
  .subscribe((value2) => {
    /* do something */ 
  });

Poiché il selettore dei risultati di switchMap è stato deprezzato, ecco una versione aggiornata

const one$ = someObservable.pipe(take(1));
const two$ = someOtherObservable.pipe(
  take(1),
  switchMap(value2 => one$.map(_ => value2))
);
two$.subscribe(value2 => {
  /* do something */ 
});

8

Ecco un modo riutilizzabile per farlo (è un dattiloscritto ma puoi adattarlo a js):

export function waitFor<T>(signal: Observable<any>) {
    return (source: Observable<T>) =>
        new Observable<T>(observer =>
            signal.pipe(first())
                .subscribe(_ =>
                    source.subscribe(observer)
                )
        );
}

e puoi usarlo come qualsiasi operatore:

var two = someOtherObservable.pipe(waitFor(one), take(1));

È fondamentalmente un operatore che rinvia la sottoscrizione sulla sorgente osservabile fino a quando il segnale osservabile emette il primo evento.


esiste una versione rxswift di questa funzione riutilizzabile
sujith1406

6

Se il secondo osservabile è caldo , c'è un altro modo per mettere in pausa / riprendere :

var pauser = new Rx.Subject();
var source1 = Rx.Observable.interval(1000).take(1);
/* create source and pause */
var source2 = Rx.Observable.interval(1000).pausable(pauser);

source1.doOnCompleted(function () { 
  /* resume paused source2 */ 
  pauser.onNext(true);
}).subscribe(function(){
  // do something
});

source2.subscribe(function(){
  // start to recieve data 
});

Inoltre è possibile utilizzare la versione bufferizzata pausableBuffered per mantenere i dati durante la pausa.


2

Eccone ancora un altro, ma mi sento più diretto e intuitivo (o almeno naturale se sei abituato a Promesse), approccio. Fondamentalmente, crei un osservabile usando Observable.create()per avvolgere onee twocome un singolo osservabile. Questo è molto simile a come Promise.all()potrebbe funzionare.

var first = someObservable.take(1);
var second = Observable.create((observer) => {
  return first.subscribe(
    function onNext(value) {
      /* do something with value like: */
      // observer.next(value);
    },
    function onError(error) {
      observer.error(error);
    },
    function onComplete() {
      someOtherObservable.take(1).subscribe(
        function onNext(value) {
          observer.next(value);
        },
        function onError(error) {
          observer.error(error);
        },
        function onComplete() {
          observer.complete();
        }
      );
    }
  );
});

Allora, cosa sta succedendo qui? Innanzitutto, creiamo un nuovo Observable. La funzione passata Observable.create(), giustamente chiamata onSubscription, viene passata all'osservatore (costruito dai parametri a cui si passa subscribe()), che è simile a resolveereject combinato in un unico oggetto quando si crea un nuovo Promise. È così che facciamo funzionare la magia.

In onSubscription, ci iscriviamo al primo Observable (nell'esempio sopra, questo è stato chiamato one). Come gestiamo nexte errordipende da te, ma l'impostazione predefinita fornita nel mio campione dovrebbe essere appropriata in generale. Tuttavia, quando riceviamo l' completeevento, il che significaone ora è , possiamo iscriverci al prossimo Observable; sparando così il secondo Observable dopo che il primo è stato completato.

L'esempio di osservatore fornito per il secondo Observable è abbastanza semplice. Fondamentalmente, secondora si comporta come ci si aspetterebbe twodi comportarsi nell'OP. Più specificamente, secondemetterà il primo e unico valore emesso da someOtherObservable(a causa ditake(1) ) e quindi completo, assumendo che non ci siano errori.

Esempio

Ecco un esempio completo e funzionante che puoi copiare / incollare se vuoi vedere il mio esempio lavorare nella vita reale:

var someObservable = Observable.from([1, 2, 3, 4, 5]);
var someOtherObservable = Observable.from([6, 7, 8, 9]);

var first = someObservable.take(1);
var second = Observable.create((observer) => {
  return first.subscribe(
    function onNext(value) {
      /* do something with value like: */
      observer.next(value);
    },
    function onError(error) {
      observer.error(error);
    },
    function onComplete() {
      someOtherObservable.take(1).subscribe(
        function onNext(value) {
          observer.next(value);
        },
        function onError(error) {
          observer.error(error);
        },
        function onComplete() {
          observer.complete();
        }
      );
    }
  );
}).subscribe(
  function onNext(value) {
    console.log(value);
  },
  function onError(error) {
    console.error(error);
  },
  function onComplete() {
    console.log("Done!");
  }
);

Se guardi la console, l'esempio sopra verrà stampato:

1

6

Fatto!


Questa è stata la svolta di cui avevo bisogno per creare il mio operatore "cluster (T, X, D)" personalizzato che elabora solo la prima X emessa entro l'intervallo di tempo T dalla sorgente ed emette risultati intervallati da un ritardo D. Grazie!
wonkim00

Sono contento che abbia aiutato, è stato molto illuminante anche quando l'ho capito.
c1moore

2

Ecco un operatore personalizzato scritto con TypeScript che attende un segnale prima di emettere risultati:

export function waitFor<T>(
    signal$: Observable<any>
) {
    return (source$: Observable<T>) =>
        new Observable<T>(observer => {
            // combineLatest emits the first value only when
            // both source and signal emitted at least once
            combineLatest([
                source$,
                signal$.pipe(
                    first(),
                ),
            ])
                .subscribe(([v]) => observer.next(v));
        });
}

Puoi usarlo in questo modo:

two.pipe(waitFor(one))
   .subscribe(value => ...);

1
bel modello! Puoi anche fare tre pipe (waitFor (uno), waitFor (due), prendi (1))
David Rinck

1

beh, so che questo è piuttosto vecchio ma penso che ciò di cui potresti aver bisogno sia:

var one = someObservable.take(1);

var two = someOtherObservable.pipe(
  concatMap((twoRes) => one.pipe(mapTo(twoRes))),
  take(1)
).subscribe((twoRes) => {
   // one is completed and we get two's subscription.
})

0

Puoi usare il risultato emesso dal precedente Observable grazie all'operatore mergeMap (o il suo alias flatMap ) in questo modo:

 const one = Observable.of('https://api.github.com/users');
 const two = (c) => ajax(c);//ajax from Rxjs/dom library

 one.mergeMap(two).subscribe(c => console.log(c))

1
da qui: learnrxjs.io/learn-rxjs/operators/transformation/mergemap - "Se l'ordine di emissione e sottoscrizione delle osservabili interne è importante, prova concatMap!"
gsziszi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.