Get current value from Observable without subscribing (just want value one time)

前端 未结 5 399
忘了有多久
忘了有多久 2020-12-13 17:15

The title says it all really. How can I get the current value from an Observable without subscribing to it? I just want the current value one time and not new values as they

相关标签:
5条回答
  • 2020-12-13 17:28

    You need to use BehaviorSubject,

    • BehaviorSubject is similar to ReplaySubject except it only remembers the last publication.
    • BehaviorSubject also requires you to provide it a default value of T. This means that all subscribers will receive a value immediately (unless it is already completed).

    It will give you the most recent value published by the Observable.

    BehaviorSubject provides a getter property named value to get the most recent value passed through it.


    StackBlitz

    • In this example the value 'a' is written to the console:

    //Declare a Subject, you'll need to provide a default value.
    const subject: BehaviorSubject<string> = new BehaviorSubject("a");
    

    USAGE:

    console.log(subject.value); // will print the current value
    

    Conceal the Subject and only expose it's value

    In case you want to conceal your BehaviorSubject and only expose it's value, let's say from a Service, you can use a getter like this.

    export class YourService {
      private subject = new BehaviorSubject('random');
    
      public get subjectValue() {
        return this.subject.value;
      }
    }
    
    0 讨论(0)
  • 2020-12-13 17:30

    Not sure if this is what you are looking for. Probably write that behaviorsubject in a service. Declare it as a private and expose only the value that you have set. Something like this

     @Injectable({
       providedIn: 'root'
     })
      export class ConfigService {
        constructor(private bname:BehaviorSubject<String>){
           this.bname = new BehaviorSubject<String>("currentvalue");
        }
    
        getAsObservable(){
           this.bname.asObservable();
        }
     }
    

    This way the external users only have access to subscribe to the behaviorSubject and you get to set the desired value in the service.

    0 讨论(0)
  • 2020-12-13 17:36

    Use the Observable constructor to create an observable stream of any type. The constructor takes as its argument the subscriber function to run when the observable’s subscribe() method executes. A subscriber function receives an Observer object, and can publish values to the observer's next() method.Try this

    @Component({
      selector: 'async-observable-pipe',
      template: '<div><code>observable|async</code>: Time: {{ time | async }} . 
    </div>'
    })
    export class AsyncObservablePipeComponent {
      time = new Observable<string>((observer: Observer<string>) => {
        setInterval(() => observer.next(new Date().toString()), 1000);
      });
    }
    
    0 讨论(0)
  • 2020-12-13 17:50

    Quick answer:

    ...I just want the current value one time and not new values as they are coming in...

    You will still use subscribe, but with pipe(take(1)) so it gives you one single value.

    eg. myObs$.pipe(take(1)).subscribe(value => alert(value));

    Also see: Comparison between first(), take(1) or single()


    Longer answer:

    The general rule is you should only ever get a value out of an observable with subscribe()

    (or async pipe if using Angular)

    BehaviorSubject definitely has its place, and when I started with RxJS I used to often do bs.value() to get a value out. As your RxJS streams propagate throughout your whole application (and that's what you want!) then it will become harder and harder to do this. Often you'll actually see .asObservable() used to 'hide' the underlying type to prevent someone from using .value() - and at first this will seem mean, but you'll start to appreciate why it's done over time. In addition you'll sooner or later need a value of something that isn't a BehaviorSubject and there won't be a way to make it so.

    Back to the original question though. Especially if you don't want to 'cheat' by using a BehaviorSubject.

    The better approach is always to use subscribe to get a value out.

    obs$.pipe(take(1)).subscribe(value => { ....... })

    OR

    obs$.pipe(first()).subscribe(value => { ....... })

    The difference between these two being first() will error if the stream has already completed and take(1) will not emit any observables if the stream has completed or doesn't have a value immediately available.

    Note: This is considered better practice even if you are are using a BehaviorSubject.

    However, if you try the above code the observable's 'value' will be 'stuck' inside the subscribe function's closure and you may well need it in the current scope. One way around this if you really have to is this:

    const obsValue = undefined;
    const sub = obs$.pipe(take(1)).subscribe(value => obsValue = value);
    sub.unsubscribe();
    
    // we will only have a value here if it was IMMEDIATELY available
    alert(obsValue);
    

    Important to note that the subscribe call above doesn't wait for a value. If nothing is available right away then the subscribe function won't ever get called, and I put the unsubscribe call there deliberately to prevent it 'appearing later'.

    So not only does this look remarkably clumsy - it won't work for something that isn't immediately available, like a result value from an http call, but it would in fact work with a behavior subject (or more importantly something that is *upstream and known to be a BehaviorSubject**, or a combineLatest that takes two BehaviorSubject values). And definitely don't go doing (obs$ as BehaviorSubject)- ugh!

    This previous example is still considered a bad practice in general - it's a mess. I only do the previous code style if I want to see if a value is available immediately and be able to detect if it isn't.

    Best approach

    You're far better off if you can to keep everything as an observable as long as possible - and only subscribe when you absolutely need the value - and not try to 'extract' a value into a containing scope which is what I'm doing above.

    eg. Lets' say we want to make a report of our animals, if your zoo is open. You might think you want the 'extracted' value of zooOpen$ like this:

    Bad way

    zooOpen$: Observable<boolean> = of(true);    // is the zoo open today?
    bear$: Observable<string> = of('Beary');
    lion$: Observable<string> = of('Liony');
    
    runZooReport() {
    
       // we want to know if zoo is open!
       // this uses the approach described above
    
       const zooOpen: boolean = undefined;
       const sub = this.zooOpen$.subscribe(open => zooOpen = open);
       sub.unsubscribe();
    
       // 'zooOpen' is just a regular boolean now
       if (zooOpen) 
       {
          // now take the animals, combine them and subscribe to it
          combineLatest(this.bear$, this.lion$).subscribe(([bear, lion]) => {
    
              alert('Welcome to the zoo! Today we have a bear called ' + bear + ' and a lion called ' + lion); 
          });
       }
       else 
       {
          alert('Sorry zoo is closed today!');
       }
    }
    

    So why is this SO BAD

    • What if zooOpen$ comes from a webservice? How will the previous example ever work? It actually wouldn't matter how fast your server is - you'd never get a value with the above code if zooOpen$ was an http observable!
    • What if you want to use this report 'outside' this function. You've now locked away the alert into this method. If you have to use the report elsewhere you'd have to duplicate this!

    Good way

    Instead of trying to access the value in your function, consider instead a function that creates a new Observable and doesn't even subscribe to it!

    It instead returns a new observable that can be consumed 'outside'.

    By keeping everything as observables and using switchMap to make decisions you can create new observables that can themselves be the source of other observables.

    getZooReport() {
    
      return this.zooOpen$.pipe(switchMap(zooOpen => {
    
         if (zooOpen) {
    
             return combineLatest(this.bear$, this.lion$).pipe(map(([bear, lion] => {
    
                     // this is inside 'map' so return a regular string
                     return "Welcome to the zoo! Today we have a bear called ' + bear + ' and a lion called ' + lion;
                  }
              );
          }
          else {
    
             // this is inside 'switchMap' so *must* return an observable
             return of('Sorry the zoo is closed today!');
          }
    
       });
     }
    

    The above creates a new observable so we can run it elsewhere, and pipe it more if we wish.

     const zooReport$ = this.getZooReport();
     zooReport$.pipe(take(1)).subscribe(report => {
        alert('Todays report: ' + report);
     });
    
     // or take it and put it into a new pipe
     const zooReportUpperCase$ = zooReport$.pipe(map(report => report.toUpperCase()));
    

    Note the following:

    • I don't subscribe until I absolutely need to - in this case that's outside the function
    • The 'driving' observable is zooOpen$ and that uses switchMap to 'switch' to a different observable which is ultimately the one returned from getZooReport().
    • The way this works if zooOpen$ ever changes then it cancels everything and starts again inside the first switchMap. Read up about switchMap for more about that.
    • Note: The code inside switchMap must return a new observable. You can make one quickly with of('hello') - or return another observable such as combineLatest.
    • Likewise: map must just returns a regular string.

    As soon I started making a mental note not to subscribe until I had to I suddenly started writing much more productive, flexible, cleaner and maintainable code.

    Another final note: If you use this approach with Angular you could have the above zoo report without a single subscribe by using the | async pipe. This is a great example of the 'don't subscribe until you HAVE to' principal in practice.

    // in your angular .ts file for a component
    const zooReport$ = this.getZooReport();
    

    and in your template:

    <pre> {{ zooReport$ | async }} </pre>
    

    See also my answer here:

    https://stackoverflow.com/a/54209999/16940

    Also not mentioned above to avoid confusion:

    • tap() may be useful sometimes to 'get the value out of an observable'. If you aren't familiar with that operator read into it. RxJS uses 'pipes' and a tap() operator is a way to 'tap into the pipe' to see what's there.
    0 讨论(0)
  • 2020-12-13 17:50

    const value = await this.observableMethod().toPromise();

    0 讨论(0)
提交回复
热议问题