javascript - Er det god praksis å bruke Observable med async/await?
Jeg bruker Angular 2 common http som returnerer en Observable, men jeg møter et problem der koden min ser ut som et nett når jeg bruker nestede Observable-kall:
this.serviceA.get().subscribe((res1: any) => {
this.serviceB.get(res1).subscribe((res2: any) => {
this.serviceC.get(res2).subscribe((res3: any) => {
})
})
})
Nå vil jeg bruke async/await for å unngå det, men async/await fungerer bare med Promise. Jeg vet at Observable kan konverteres til Promise, men så vidt jeg vet er det ikke en god praksis. Så hva bør jeg gjøre her?
Forresten, det hadde vært fint om noen kunne gi meg et eksempel på kode for å løse dette med async/await :D
Svar #1
Kjedende observabler i rekkefølge, slik du vil gjøre i koden din
Når det gjelder kodeeksemplet ditt, hvis du vil kjede observables (utløse en annen etter at den forrige sender), bruk flatMap (eller switchMap) til dette formålet :
this.serviceA.get()
.flatMap((res1: any) => this.serviceB.get())
.flatMap((res2: any) => this.serviceC.get())
.subscribe( (res3: any) => {
....
});
Denne er bedre praksis sammenlignet med nesting, da dette vil gjøre ting klarere og hjelpe deg å unngå callback-helvete, som Observable og Promises egentlig skulle bidra til å forhindre i utgangspunktet.
Vurder også å bruke switchMap i stedet for flatMap, det vil i praksis tillate å «avbryte» de andre forespørslene dersom den første sender ut en ny verdi. Greit å bruke hvis den første Observable som utløser resten er et klikk på en knapp, for eksempel.
Hvis du ikke trenger at de ulike forespørslene dine skal vente på hverandre, kan du bruke forkJoin eller zip for å starte dem alle samtidig. Se @Dan Macaks svar for detaljer og andre innsikter.
Angular 'async'-pipe og Observables fungerer godt sammen
Når det gjelder Observables og Angular, kan du perfekt bruke | async-pipe i en Angular-mal i stedet for å abonnere på Observable i komponentkoden din, for å få verdien(e) som sendes ut av denne Observable.
ES6 async/await og Promises i stedet for Observables ?
hvis du ikke føler for å bruke Observable direkte, kan du enkelt bruke .toPromise() på Observable-en din, og deretter noen async/await-instruksjoner.
Hvis Observablen din skal returnere bare ett resultat (som er tilfelle med grunnleggende API-kall), kan en Observable ses på som ganske ekvivalent med et Promise.
Imidlertid er jeg ikke sikker på om det er nødvendig å gjøre det, med tanke på alt det Observable allerede tilbyr (til leserne: opplysende moteksempler er velkomne!). Jeg ville heller være mer for å bruke Observables når du kan, som en treningsøvelse.
Noen interessante blogginnlegg om det (og det finnes mange andre):
https://medium.com/@benlesh/rxjs-observable-interop-with-promises-and-async-await-bebb05306875
toPromise-funksjonen er faktisk litt vanskelig, da det egentlig ikke er en «operator», heller er det et RxJS-spesifikt middel for å abonnere på en Observable og pakke det inn i et løfte. Løftet vil løse seg til den siste verdien som sendes ut av Observable når Observable fullfører. Det betyr at hvis Observable sender ut verdien «hi» og deretter venter 10 sekunder før den fullfører, vil det returnerte løftet vente 10 sekunder før det løses til «hi». Hvis Observable aldri fullfører, løses aldri Løftet.
MERK: Å bruke toPromise() er et antipattern unntatt i tilfeller der du håndterer et API som forventer et Promise, for eksempel async-await
(utheving min)
Eksemplet du ba om
Forresten, det hadde vært fint om noen kunne gi meg et eksempel på kode for å løse dette med async/await :D
Eksempel hvis du virkelig vil gjøre det (sannsynligvis med noen feil, kan ikke sjekke akkurat nå, vær så snill å rette dem)
// Warning, probable anti-pattern below
async myFunction() {
const res1 = await this.serviceA.get().toPromise();
const res2 = await this.serviceB.get().toPromise();
const res3 = await this.serviceC.get().toPromise();
// other stuff with results
}
I tilfelle du kan starte alle forespørsler samtidig, await Promise.all() som bør være mer effektivt, fordi ingen av kallene avhenger av resultatet av hverandre. (slik forkJoin ville gjort med Observables)
async myFunction() {
const promise1 = this.serviceA.get().toPromise();
const promise2 = this.serviceB.get().toPromise();
const promise3 = this.serviceC.get().toPromise();
let res = await Promise.all([promise1, promise2, promise3]);
// here you can retrieve promises results,
// in res[0], res[1], res[2] respectively.
}
Svar #2
Som @Pac0 allerede har utdypet de ulike løsningene godt, vil jeg bare legge til et litt annet perspektiv.
Blande løfter og observabler
Jeg foretrekker personlig å ikke blande Promises og Observables - noe som er det du får når du bruker async await med Observables, fordi selv om de ser like ut, er de veldig forskjellige.
- Løfter er alltid asynkrone, Observables ikke nødvendigvis
- Løfter representerer bare 1 verdi, Observables 0, 1 eller mange
- Løfter har svært begrenset bruk, du kan for eksempel ikke avbryte dem (legg til side ES sine neste forslag), Observables er så mye kraftigere i bruken sin (du kan for eksempel håndtere flere WS-tilkoblinger med dem, prøv det med Løfter)
- Deres API-er er svært forskjellige
Bruk av løfter i Angular
Nå, selv om det noen ganger er gyldig å bruke begge, spesielt med Angular synes jeg man bør vurdere å gå så langt som mulig med RxJS. Årsakene er:
- Stor del av Angular API bruker Observables (router, http ...), så man går på en måte med strømmen (ingen ordspill ment) ved å bruke RxJS, ellers måtte man konvertere til Promises hele tiden samtidig som man går glipp av mulighetene RxJS gir
- Angular har en kraftig
async-pipe som gjør det mulig å komponere hele applikasjonens datastream, som du kan filtrere, kombinere og gjøre hva som helst med uten å avbryte datastrømmen som kommer fra serveren uten noe behov for thening eller subscribing. På denne måten trenger du ikke pakke ut dataene eller tilordne dem til hjelpvariabler, dataene flyter bare fra tjenester gjennom Observables direkte til malen, noe som er helt nydelig.
Det finnes noen tilfeller der Promise fortsatt kan skinne. For eksempel det jeg savner i rxjs TypeScript-typer er konseptet single. Hvis du lager et API som andre skal bruke, sier det ikke så mye å returnere Observable: Vil du motta 1 verdi, mange, eller vil det bare fullføre? Du må skrive en kommentar for å forklare det. På den annen side har Promise en mye tydeligere kontrakt i dette tilfellet. Den vil alltid løses med 1 verdi eller avvises med en feil (med mindre den selvfølgelig henger for alltid).
Generelt sett trenger du absolutt ikke å ha kun Promises eller kun Observables i prosjektet ditt. Hvis du bare vil uttrykke med en verdi at noe ble fullført (slette bruker, oppdatere bruker), og du vil reagere på det uten å integrere det i en strøm, er Promise den mest naturlige måten å gjøre det på. Dessuten gir bruk av async/await deg muligheten til å skrive kode på en sekvensiell måte og dermed forenkle det betydelig, så med mindre du trenger avansert håndtering av innkommende verdier, kan du holde deg til Promise.
Tilbake til ditt eksempel
Så min anbefaling er å omfavne både kraften i RxJS og Angular. Tilbake til ditt eksempel, kan du skrive koden som følger (kreditt for ideen til @Vayrex):
this.result$ = Observable.forkJoin(
this.serviceA.get(),
this.serviceB.get(),
this.serviceC.get()
);
this.result$.subscribe(([resA, resB, resC]) => ...)
Dette kodeeksempelet vil sende 3 forespørsler, og når alle disse forespørsels-Observables er fullført, vil abonnements-callbacken til forkJoin gi deg resultatene i en array, og som nevnt, kan du abonnere på det manuelt (som i eksemplet) eller gjøre dette deklarativt ved å bruke result$ og async-pipe i malen.
Å bruke Observable.zip ville gi deg samme resultat her, forskjellen mellom forkJoin og zip er at førstnevnte kun sender ut de siste verdiene fra de indre Observables, mens sistnevnte kombinerer de første verdiene fra de indre Observables, deretter de andre verdiene osv.
Rediger: Siden du trenger resultatene fra tidligere HTTP-forespørsler, bruk flatMap-tilnærmingen i @Pac0s svar.
Svar #3
Siden toPromise nå er avskrevet i 2022, vil jeg vise en annen måte å bruke await på et observable. Jeg synes denne metoden gjør koden mye mer lesbar, i motsetning til lange, komplekse rxjs-pipelines. Dette er spesielt nyttig for HTTP-forespørsler siden det kun er ett svar, og man generelt ønsker å vente på svaret før man gjør noe annet.
Oppdatering
Min opprinnelige løsning fungerer, men rxjs har i utgangspunktet den samme funksjonen: firstValueFrom().
Fra dokumentasjonen:
async function execute() {
const source$ = interval(2000);
const firstNumber = await firstValueFrom(source$);
console.log(`The first number is ${firstNumber}`);
}
Opprinnelig løsning
Hvis du har en observable, kan du pakke den inn i et løfte, abonnere og løse når abonnementet sender ut.
getSomething(): Promise<any> {
return new Promise((resolve, reject) => {
this.http
.get('www.myApi.com')
.subscribe({
next: (data) => resolve(data),
error: (err) => reject(err),
});
});
}
Nå kan vi vente på et svar inne i en async-funksjon
async ngOnInit() {
const data = await this.getSomething();
//Do something with your data
}
Nå kan vi utføre mange komplekse operasjoner på dataene, og det vil være mye mer lesbart for folk som ikke er rxjs-eksperter. Hvis du hadde tre etterfølgende HTTP-forespørsler som var avhengige av hverandre, ville det se slik ut:
async ngOnInit() {
const first = await this.getFirst();
const second = await this.getSecond(first);
const third = await this.getThird(second);
}
