Come ottenere il valore corrente di RxJS Subject o Observable?


207

Ho un servizio Angular 2:

import {Storage} from './storage';
import {Injectable} from 'angular2/core';
import {Subject}    from 'rxjs/Subject';

@Injectable()
export class SessionStorage extends Storage {
  private _isLoggedInSource = new Subject<boolean>();
  isLoggedIn = this._isLoggedInSource.asObservable();
  constructor() {
    super('session');
  }
  setIsLoggedIn(value: boolean) {
    this.setItem('_isLoggedIn', value, () => {
      this._isLoggedInSource.next(value);
    });
  }
}

Funziona tutto alla grande. Ma ho un altro componente che non ha bisogno di abbonarsi, deve solo ottenere il valore corrente di isLoggedIn in un determinato momento. Come posso fare questo?

Risposte:


341

A Subjecto Observablenon ha un valore corrente. Quando viene emesso un valore, viene passato agli abbonati e Observableviene terminato.

Se si desidera avere un valore corrente, utilizzare quello BehaviorSubjectprogettato esattamente per quello scopo. BehaviorSubjectmantiene l'ultimo valore emesso e lo emette immediatamente ai nuovi abbonati.

Ha anche un metodo getValue()per ottenere il valore corrente.


1
Ciao, è un peccato, adesso sono la stessa cosa in rxjs 5. Il link del codice sorgente sopra si riferiva a rxjs4.
il codice di Schrodinger il

84
Nota importante dell'autore di RxJS 5: l'utilizzo di getValue()una bandiera rossa ENORME sta facendo qualcosa di sbagliato. È lì come una botola di fuga. Generalmente tutto ciò che fai con RxJS dovrebbe essere dichiarativo. getValue()è imperativo. Se stai usando getValue(), c'è una probabilità del 99,9% che stai facendo qualcosa di sbagliato o strano.
Ben Lesh,

1
@BenLesh cosa succede se ho un BehaviorSubject<boolean>(false)e mi piace attivarlo ?
bob

3
@BenLesh getValue () è molto utile per dire, facendo un'azione istantanea, come onClick-> dispatchToServer (x.getValue ()) ma non usarlo in catene osservabili.
FlavorScape

2
@ IngoBürk L'unico modo per giustificare il tuo suggerimento è se non sapessi che foo$era un BehaviorSubject(cioè è definito come Observablee forse è caldo o freddo da una fonte differita). Tuttavia dal momento che poi andare a uso .nextsu $foosé che significa che l'implementazione si basa su di esso di essere un BehaviorSubjectcosì non c'è alcuna giustificazione per non solo con .valueper ottenere il valore corrente, in primo luogo.
Simon_Weaver,

145

L'unico modo in cui dovresti ottenere valori "fuori da" un osservabile / soggetto è con iscriviti!

Se stai usando getValue()stai facendo qualcosa di imperativo nel paradigma dichiarativo. È lì come tratteggio di fuga, ma il 99,9% delle volte NON dovresti usarlo getValue(). Ci sono alcune cose interessanti che getValue()faranno: genererà un errore se il soggetto è stato cancellato, ti impedirà di ottenere un valore se il soggetto è morto perché è stato errato, ecc. Ma, ancora una volta, è lì come una via di fuga schiusa per rare circostanze.

Esistono diversi modi per ottenere l'ultimo valore da un soggetto o osservabile in modo "Rx-y":

  1. Utilizzo BehaviorSubject: ma in realtà iscrivendoti ad esso . Quando ti iscrivi per la prima BehaviorSubjectvolta, invia in modo sincrono il valore precedente che ha ricevuto o con cui è stato inizializzato.
  2. Utilizzo di ReplaySubject(N): Questo memorizzerà i Nvalori nella cache e li riprodurrà ai nuovi abbonati.
  3. A.withLatestFrom(B): Utilizzare questo operatore per ottenere il valore più recente da osservabile Bquando viene Aemesso osservabile . Ti darà entrambi i valori in un array [a, b].
  4. A.combineLatest(B): Utilizzare questo operatore per ottenere i valori più recenti da Ae Bogni volta Ao Bemette. Ti darà entrambi i valori in un array.
  5. shareReplay(): Crea un multicast osservabile tramite a ReplaySubject, ma consente di riprovare l'osservabile in caso di errore. (Fondamentalmente ti dà quel comportamento di cache promettente).
  6. publishReplay(), publishBehavior(initialValue), multicast(subject: BehaviorSubject | ReplaySubject), Ecc: Altri operatori che sfruttano BehaviorSubjecte ReplaySubject. Diversi gusti della stessa cosa, fondamentalmente multicast la fonte osservabile incanalando tutte le notifiche attraverso un argomento. Devi chiamare connect()per iscriverti alla fonte con l'oggetto.

19
puoi elaborte perché usare getValue () è una bandiera rossa? Cosa succede se ho bisogno del valore corrente dell'osservabile solo una volta, nel momento in cui l'utente fa clic su un pulsante? Devo iscrivermi per il valore e cancellare immediatamente l'iscrizione?
Fiamma

5
Va bene a volte. Ma spesso è un segno che l'autore del codice sta facendo qualcosa di molto imperativo (di solito con effetti collaterali) dove non dovrebbero essere. Puoi fare anche click$.mergeMap(() => behaviorSubject.take(1))per risolvere il tuo problema.
Ben Lesh,

1
Grande spiegazione! In realtà, se puoi mantenere Observable e usare i metodi, è un modo molto migliore. In un contesto di dattiloscritto con Observable usato ovunque ma solo alcuni definiti come Oggetto Comportamento, sarebbe un codice meno consistente. I metodi che mi hai proposto mi hanno permesso di mantenere i tipi osservabili ovunque.
JLavoie,

2
è utile se si desidera semplicemente inviare il valore corrente (come inviarlo al server con un clic), fare getValue () è molto utile. ma non usarlo quando si concatenano operatori osservabili.
FlavorScape

1
Ho un AuthenticationServiceche usa un BehaviourSubjectper memorizzare lo stato di accesso corrente ( boolean trueo false). Espone un isLoggedIn$osservabile per gli abbonati che vogliono sapere quando cambia lo stato. Espone anche una get isLoggedIn()proprietà, che restituisce lo stato di accesso corrente chiamando getValue()il sottostante BehaviourSubject: questo viene utilizzato dalla mia guardia di autenticazione per verificare lo stato corrente. Questo mi sembra un uso sensato getValue()...?
Dan King,

13

Ho avuto una situazione simile in cui gli abbonati in ritardo si iscrivono all'oggetto dopo che è arrivato il suo valore.

Ho trovato ReplaySubject che è simile a BehaviorSubject funziona come un incantesimo in questo caso. Ed ecco un link per una migliore spiegazione: http://reactivex.io/rxjs/manual/overview.html#replaysubject


1
che mi ha aiutato nella mia app Angular4 - ho dovuto spostare l'abbonamento dal costruttore del componente a ngOnInit () (tale componente è condiviso tra le rotte), lasciandolo qui nel caso in cui qualcuno abbia un problema simile
Luca

1
Ho avuto un problema con un'app Angular 5 in cui stavo usando un servizio per ottenere valori da un'API e impostare variabili in diversi componenti. Stavo usando soggetti / osservabili ma non spingevo i valori dopo un cambio di rotta. ReplaySubject è stato un calo in sostituzione di Subject e risolto tutto.
jafaircl,

1
Assicurati di utilizzare ReplaySubject (1) altrimenti i nuovi abbonati otterranno ogni valore precedentemente emesso in sequenza - questo non è sempre ovvio in fase di esecuzione
Drenai

@Drenai per quanto ho capito ReplaySubject (1) si comportano allo stesso modo di BehaviorSubject ()
Kfir Erez

Non è la stessa cosa, la grande differenza è che ReplaySubject non emette immediatamente un valore predefinito quando è sottoscritto se la sua next()funzione non è stata ancora invocata, mentre BehaviourSubject lo fa. L'emissione immediata è molto utile per l'applicazione di valori predefiniti a una vista, quando BehaviourSubject viene utilizzato come origine dati osservabile in un servizio, ad esempio
Drenai

5
const observable = of('response')

function hasValue(value: any) {
  return value !== null && value !== undefined;
}

function getValue<T>(observable: Observable<T>): Promise<T> {
  return observable
    .pipe(
      filter(hasValue),
      first()
    )
    .toPromise();
}

const result = await getValue(observable)
// Do the logic with the result
// .................
// .................
// .................

Puoi consultare l'articolo completo su come implementarlo da qui. https://www.imkrish.com/how-to-get-current-value-of-observable-in-a-clean-way/


3

Ho riscontrato lo stesso problema nei componenti figlio in cui inizialmente avrebbe dovuto avere il valore corrente dell'oggetto, quindi sottoscrivere l'oggetto per ascoltare le modifiche. Mantengo semplicemente il valore corrente nel Servizio in modo che sia disponibile per l'accesso ai componenti, ad esempio:

import {Storage} from './storage';
import {Injectable} from 'angular2/core';
import {Subject}    from 'rxjs/Subject';

@Injectable()
export class SessionStorage extends Storage {

  isLoggedIn: boolean;

  private _isLoggedInSource = new Subject<boolean>();
  isLoggedIn = this._isLoggedInSource.asObservable();
  constructor() {
    super('session');
    this.currIsLoggedIn = false;
  }
  setIsLoggedIn(value: boolean) {
    this.setItem('_isLoggedIn', value, () => {
      this._isLoggedInSource.next(value);
    });
    this.isLoggedIn = value;
  }
}

Un componente che necessita del valore corrente potrebbe accedervi dal servizio, ad esempio:

sessionStorage.isLoggedIn

Non sono sicuro se questa è la pratica giusta :)


2
Se hai bisogno del valore di un osservabile nella vista dei componenti, puoi semplicemente usare la asyncpipe.
Ingo Bürk,

2

Una simile ricerca risposta è stata downvoted. Ma penso di poter giustificare ciò che sto suggerendo qui per casi limitati.


Mentre è vero che un osservabile non ha un valore corrente , molto spesso avrà un valore immediatamente disponibile . Ad esempio con i negozi redux / flux / akita è possibile richiedere dati a un negozio centrale, in base a un numero di osservabili e quel valore sarà generalmente immediatamente disponibile.

Se questo è il caso allora subscribe, il valore tornerà immediatamente.

Supponiamo quindi che tu abbia ricevuto una chiamata a un servizio e, al termine, desideri ottenere l'ultimo valore di qualcosa dal tuo negozio, che potenzialmente potrebbe non emettere :

Potresti provare a farlo (e dovresti, per quanto possibile, tenere le cose 'dentro le pipe'):

 serviceCallResponse$.pipe(withLatestFrom(store$.select(x => x.customer)))
                     .subscribe(([ serviceCallResponse, customer] => {

                        // we have serviceCallResponse and customer 
                     });

Il problema è che si bloccherà fino a quando l'osservabile secondario non emetterà un valore, che potenzialmente potrebbe non essere mai.

Mi sono ritrovato di recente a dover valutare un osservabile solo se un valore era immediatamente disponibile e, soprattutto, dovevo essere in grado di rilevare se non lo fosse. Ho finito per fare questo:

 serviceCallResponse$.pipe()
                     .subscribe(serviceCallResponse => {

                        // immediately try to subscribe to get the 'available' value
                        // note: immediately unsubscribe afterward to 'cancel' if needed
                        let customer = undefined;

                        // whatever the secondary observable is
                        const secondary$ = store$.select(x => x.customer);

                        // subscribe to it, and assign to closure scope
                        sub = secondary$.pipe(take(1)).subscribe(_customer => customer = _customer);
                        sub.unsubscribe();

                        // if there's a delay or customer isn't available the value won't have been set before we get here
                        if (customer === undefined) 
                        {
                           // handle, or ignore as needed
                           return throwError('Customer was not immediately available');
                        }
                     });

Nota che per quanto sopra sto usando subscribeper ottenere il valore (come discute da @Ben). Non usare una .valueproprietà, anche se avevo un BehaviorSubject.


1
A proposito, questo funziona perché di default 'schedule' usa il thread corrente.
Simon_Weaver,

1

Anche se può sembrare eccessivo, questa è solo un'altra soluzione "possibile" per mantenere il tipo osservabile e ridurre la piastra della caldaia ...

Puoi sempre creare un getter di estensione per ottenere il valore corrente di un osservabile.

Per fare ciò è necessario estendere l' Observable<T>interfaccia in un global.d.tsfile di dichiarazione delle tipizzazioni. Quindi implementare il getter dell'estensione in un observable.extension.tsfile e infine includere sia le tipizzazioni che il file dell'estensione nell'applicazione.

È possibile fare riferimento a questa risposta StackOverflow per sapere come includere le estensioni nell'applicazione angolare.

// global.d.ts
declare module 'rxjs' {
  interface Observable<T> {
    /**
     * _Extension Method_ - Returns current value of an Observable.
     * Value is retrieved using _first()_ operator to avoid the need to unsubscribe.
     */
    value: Observable<T>;
  }
}

// observable.extension.ts
Object.defineProperty(Observable.prototype, 'value', {
  get <T>(this: Observable<T>): Observable<T> {
    return this.pipe(
      filter(value => value !== null && value !== undefined),
      first());
  },
});

// using the extension getter example
this.myObservable$.value
  .subscribe(value => {
    // whatever code you need...
  });

0

È possibile memorizzare l'ultimo valore emesso separatamente dall'Osservabile. Quindi leggilo quando necessario.

let lastValue: number;

const subscription = new Service().start();
subscription
    .subscribe((data) => {
        lastValue = data;
    }
);

1
Non è un approccio reattivo per conservare alcune cose al di fuori dell'osservabile. Invece dovresti avere quanti più dati possibili fluiscono all'interno dei flussi osservabili.
ganqqwerty,

0

Il modo migliore per farlo è usare Behaviur Subject, ecco un esempio:

var sub = new rxjs.BehaviorSubject([0, 1])
sub.next([2, 3])
setTimeout(() => {sub.next([4, 5])}, 1500)
sub.subscribe(a => console.log(a)) //2, 3 (current value) -> wait 2 sec -> 4, 5

0

È possibile creare un abbonamento e dopo aver eliminato il primo elemento emesso. Pipe è una funzione che utilizza un osservabile come input e restituisce un altro osservabile come output, senza modificare il primo osservabile. Angolare 8.1.0. Pacchetti: "rxjs": "6.5.3","rxjs-observable": "0.0.7"

  ngOnInit() {

    ...

    // If loading with previously saved value
    if (this.controlValue) {

      // Take says once you have 1, then close the subscription
      this.selectList.pipe(take(1)).subscribe(x => {
        let opt = x.find(y => y.value === this.controlValue);
        this.updateValue(opt);
      });

    }
  }
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.