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.