问题
I have a service which loads some data in it's constructor with an Observable. Then at some later time the data can be retrieved with a getter. It should return the data right away if it's present. Or wait for loading to finish if that's still in progress. I came up with following example (code is in Typescript):
class MyService {
private loadedEvent = new Subject<any>();
private loaded = false;
private data: any;
constructor(
private dataService: DataService
) {
this.dataService.loadData().subscribe(data => {
this.data = data;
this.loaded = true;
this.loadedEvent.next(null);
});
}
public getDataOrWait(): Observable<any> {
if (this.loaded) { // A
return of(this.data);
}
return new Observable((observer: Observer<any>) => {
const subscription = this.loadedEvent.subscribe(() => { // B
observer.next(this.data);
observer.complete();
subscription.unsubscribe();
});
});
}
}
Is there a simpler way to do this? This must be a common pattern.
Also, I think there is a race condition if loading finishes when execution is somewhere between the lines marked A and B (I am not sure if threads are involved here - the data is loaded async however).
回答1:
It seems that you simply want to logically extend the Observable-based interface of your data service to the clients of your MyService class. You could use a new AsyncSubject, which will emit a single value to all subscribers once it has completed.
class MyService {
private data: any;
private dataSubject = new AsyncSubject<any>();
constructor(
private dataService: DataService
) {
this.dataService.loadData().subscribe(data => {
this.data = data;
this.dataSubject.next(data);
this.dataSubject.complete();
});
}
public getData(): Observable<any> {
return this.dataSubject.asObservable();
}
}
The caller of getData
would then do something like:
service.getData().subscribe((data) => {
console.log(`got data ${data}`);
});
回答2:
All you have to do is use a shareReplay() operator:
class MyService {
public data$: Observable<any>;
public loaded$: Observable<boolean>;
constructor(private dataService: DataService) {
this.data$ = this.dataService.loadData().pipe(
shareReplay(1);
);
this.loaded$ = this.data$.pipe(
mapTo(true),
startWith(false)
);
}
}
The shareReplay
operator is a multi-casting operator that will emit the same previous value to all subscribers. Subscribers will wait until the first value is available.
You can then use that data$
to create a loaded$
observable that will emit false
until data$
finally emits a value, and then it will always emit true
when the values are ready.
Alternatively, you can have data$
emit a null
before data is ready followed by the data. There are some logical benefits downstream that allow you to create new observables for when data is ready or not.
this.data$ = this.dataService.loadData().pipe(
startWith(null),
shareReplay(1);
);
You have to call myService.data$.subscribe()
to trigger the first reading of the stream to make the data ready. You can do that in the constructor, but keep in mind that Angular doesn't create a service until it is first used. If you want the data to be eagerly loaded, then use a resolver in a route or inject the service into a NgModule
constructor and subscribe there.
回答3:
First of all you should know that observable guarantees any response from the server will be existing for you, that way your design shall consider that, simply handle your logic in subscribe, and move from that subscription. This is all is done in async mode.
But, if you are getting just single result not streaming, and want to wait till the data is loaded from server completely, then you can use Promise with sync await (this is common pattern) to get the result synchronously. By the way your scenario sounds to be Promise behavior.
If you have to use Observables, you can use forkJoin or flatMap to wait till the date is loaded completely.
Check this awesome answer with perfect comparisons
Is it a good practice using Observable with async/await?
来源:https://stackoverflow.com/questions/59593680/how-to-return-data-or-wait-if-data-loading-is-still-in-progress-with-rxjs