Hvis du bare kommer i gang med programmering, er sjansen stor for at du tenker på programmer som et sett med sekvensielle blokker med logikk, der hver blokk gjør en bestemt ting og sender resultatet slik at neste blokk kan kjøre og så videre, og for for det meste har du rett, de fleste programmer kjører på en sekvensiell måte, denne modellen lar oss bygge programmer som er enkle å skrive og vedlikeholde. Det er imidlertid spesifikke brukstilfeller der denne sekvensielle modellen ikke ville fungere, eller ikke ville være optimal. Som et eksempel kan du vurdere en bokleserapplikasjon. Denne applikasjonen har noen få avanserte funksjoner som å finne alle forekomster av et ord, navigere mellom bokmerker og lignende. Tenk deg nå at brukeren for øyeblikket leser en lang bok og bestemmer seg for å se etter alle forekomstene av et vanlig ord som "The". Programmet vil normalt ta et par sekunder å finne og indeksere alle forekomstene av det ordet. I et sekvensielt program kan ikke brukeren samhandle med applikasjonen (endre siden eller markere en tekst) før søkeoperasjonen er fullført. Forhåpentligvis kan du se at det ikke er en optimal brukeropplevelse!
Diagrammet illustrerer en typisk utførelsesflyt for bokleserapplikasjonen. Hvis brukeren starter en langvarig operasjon (i dette tilfellet søket etter alle forekomster av "den" i en stor bok), "fryser" applikasjonen for hele operasjonen. I dette tilfellet vil brukeren fortsette å klikke på neste bokmerke-knappen uten resultat før søkeoperasjonen er fullført, og alle operasjonene vil tre i kraft samtidig og gir sluttbrukeren følelsen av en hengende applikasjon.
Du har kanskje lagt merke til at dette eksemplet egentlig ikke samsvarer med den sekvensielle modellen vi introduserte tidligere. Dette er fordi driften her er uavhengig av hverandre. Brukeren trenger ikke å vite om antall forekomster av "the" for å navigere til neste bokmerke, så rekkefølgen for utførelse av operasjoner er egentlig ikke viktig. Vi trenger ikke å vente på slutten av søkeoperasjonen før vi kan navigere til neste bokmerke. En mulig forbedring av den forrige utførelsesflyten er basert på denne logikken: vi kan kjøre den lange søkeoperasjonen i bakgrunnen, fortsette med alle innkommende operasjoner, og når den lange operasjonen er utført, kan vi ganske enkelt varsle brukeren. Utførelsesflyten blir som følger:
Med denne utførelsesflyten er brukeropplevelsen betydelig forbedret. Nå kan brukeren starte en langvarig operasjon, fortsette å bruke applikasjonen normalt og bli varslet når operasjonen er fullført. Dette er grunnlaget for asynkron programmering.
Javascript, blant andre språk, støtter denne stilen med asynkron programmering ved å tilby omfattende APIer for å oppnå omtrent hvilken som helst asynkron atferd du kan tenke deg. På slutten av dagen bør Javascript være et asynkront språk. Hvis vi refererer til det forrige eksemplet, er den asynkrone logikken i bunnen av alle brukerinteraksjonsapplikasjoner, og Javascript ble først og fremst bygget for å brukes på nettleseren der de fleste programmene handler om å svare på brukerhandlinger.
Følgende vil gi deg en kort veiledning om Asynkron Javascript:
Tilbakeringinger
I et typisk program vil du vanligvis finne en rekke funksjoner. For å bruke en funksjon kaller vi den med et sett med parametere. Funksjonskoden vil kjøre og returnere et resultat, ingenting utenom det vanlige. Asynkron programmering forskyver denne logikken litt. Går tilbake til eksemplet med bokleserapplikasjonen, kan vi ikke bruke en vanlig funksjon for å implementere søkeoperasjonslogikken siden operasjonen tar en ukjent tid. En vanlig funksjon vil i utgangspunktet komme tilbake før operasjonen er utført, og det er ikke denne oppførselen vi forventer. Løsningen er å spesifisere en annen funksjon som vil bli utført når søkeoperasjonen er fullført. Dette modellerer vår brukstilfelle ettersom programmet vårt kan fortsette sin flyt normalt, og når søkeoperasjonen er fullført, vil den angitte funksjonen utføres for å varsle brukeren om søkeresultatene. Denne funksjonen er det vi kaller en tilbakeringingsfunksjon:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Først definerer vi søkeoperasjonsfunksjonen, searchOccurrences. Det krever ordet å søke etter og en andre parameter "tilbakeringing" som vil være funksjonen som skal utføres når søkeoperasjonen er fullført. Søkeoperasjonsfunksjonen ble med vilje holdt abstrakt, vi trenger bare å fokusere på de to mulige resultatene: det første tilfellet er hvor alt gikk vellykket og vi har resultatet av søket i resultatvariabelen. I dette tilfellet må vi bare kalle tilbakeringingsfunksjonen med følgende parametere: den første parameteren er null, noe som betyr at det ikke har oppstått noen feil, den andre parameteren er ordet som ble søkt på, og den tredje og kanskje viktigste parameteren av de tre, er resultatet av søkeoperasjonen.
Det andre tilfellet er hvor det oppstår en feil, dette er også et tilfelle hvor utførelsen av søkeoperasjonen er utført og vi må ringe tilbakeringingsfunksjonen. Vi bruker en try and catch-blokk for å fange opp eventuelle feil, og vi kaller bare tilbakeringingsfunksjonen med feilobjektet fra catch-blokken.
Vi definerte deretter tilbakeringingsfunksjonen, handleSearchOccurrences, vi holdt logikken ganske enkel. Det er bare å skrive ut en melding til konsollen. Vi sjekker først "err" -parameteren for å se om det oppsto noen feil i hovedfunksjonen. I så fall gir vi bare brukeren beskjed om at søkeoperasjonen endte med en feil. Hvis det ikke ble registrert feil, skriver vi ut en melding med resultatet av søkeoperasjonen.
Til slutt kaller vi søkefunksjonen Occurrences med ordet "the". Funksjonen vil nå kjøre normalt uten å blokkere hovedprogrammet og når søket er gjort, vil tilbakeringingen bli utført og vi vil få resultatmeldingen enten med søkeresultatet eller feilmeldingen.
Det er viktig å nevne her at vi kun har tilgang til resultatvariabelen inne i hoved- og tilbakeringingsfunksjonene. Hvis vi prøver noe slikt:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
resultatet av utskriften ville være udefinert fordi programmene ikke venter på at searchOccurrences-funksjonen skal utføres. Den flytter til neste instruksjon som er utskriftssetningen før resultatvariabelen tilordnes inne i hovedfunksjonen. Som et resultat vil vi skrive ut den ikke-tildelte resultatvariabelen.
Så basert på denne logikken bør vi beholde all koden som bruker resultatvariabelen inne i tilbakeringingsfunksjonen. Dette virker kanskje ikke som et problem nå, men det kan raskt eskalere til et reelt problem. Tenk deg tilfellet hvor vi har en kjede av asynkrone funksjoner som må kjøres i rekkefølge. I den typiske tilbakeringingslogikken vil du implementere noe som dette:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Husk at hver tilbakeringing har en feilparameter og hver feil må håndteres separat. Dette gjør den allerede komplekse koden ovenfor enda mer kompleks og vanskelig å vedlikeholde. Forhåpentligvis er løfter her for å løse tilbakeringingshelvete-problemet, vi vil dekke det neste.
Løfter
Løfter er bygget på toppen av tilbakeringinger og fungerer på lignende måte. De ble introdusert som en del av ES6-funksjonene for å løse noen av de store problemene med tilbakeringinger som for eksempel tilbakeringingshelvete. Løfter gir sine egne funksjoner som kjører ved vellykket gjennomføring (løse), og når feil oppstår (avvise). Følgende viser eksempelet searchOccurrences implementert med løfter:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
La oss gå gjennom endringene vi brukte:
SearchOccurrences-funksjonen returnerer et løfte. Innenfor løftet holder vi den samme logikken: vi har to funksjoner løse og avvise som representerer våre tilbakeringinger i stedet for å ha en enkelt tilbakeringingsfunksjon som håndterer både en vellykket kjøring og en kjøring med feil. Løfter skiller de to resultatene og gir en ren syntaks når du kaller hovedfunksjonen. Løsningsfunksjonen er "koblet" til hovedfunksjonen ved å bruke nøkkelordet "da". Her spesifiserer vi bare de to parameterne for løsningsfunksjonen og skriver ut søkeresultatet. En lignende ting gjelder for avvisningsfunksjonen, den kan kobles til ved å bruke søkeordet "fangst". Forhåpentligvis kan du sette pris på fordelene løftene gir når det gjelder kodelesbarhet og renslighet. Hvis du fortsatt diskuterer det, sjekk ut hvordan vi kan løse tilbakeringingshelvete-problemet ved å lenke sammen de asynkrone funksjonene for å kjøre den ene etter den andre:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Await
Async/Await er det siste tilskuddet til vårt asynkrone verktøybelte i Javascript. Introdusert med ES8, gir de et nytt lag med abstraksjon på toppen av asynkrone funksjoner ved ganske enkelt å "vente" på utførelse av en asynkron operasjon. Flyten av programmet blokkerer ved den instruksjonen inntil et resultat returneres fra den asynkrone operasjonen, og deretter vil programmet fortsette med neste instruksjon. Hvis du tenker på den synkrone utførelsesflyten, har du rett. Vi har kommet i full sirkel! Async/wait forsøk på å bringe enkelheten til synkron programmering til den asynkrone verden. Vær oppmerksom på at dette kun oppfattes i utførelsen og koden til programmet. Alt forblir det samme under panseret, Async/await bruker fortsatt løfter og tilbakeringinger er byggesteinene deres.
La oss gå gjennom eksemplet vårt og implementere det ved å bruke Async/await:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Koden vår endret seg ikke mye, det som er viktig å legge merke til her er nøkkelordet "async" før funksjonsdeklarasjonen for searchOccurrences. Dette indikerer at funksjonen er asynkron. Legg også merke til nøkkelordet "avvent" når du ringer søkefunksjonen. Dette vil instruere programmet til å vente på utførelse av funksjonen til resultatet returneres før programmet kan flytte til neste instruksjon, med andre ord, resultatvariabelen vil alltid ha den returnerte verdien av searchOccurrences-funksjonen og ikke løftet om funksjonen, i den forstand, Async/Await har ikke en ventende tilstand som Promises. Når utførelsen er fullført, går vi til utskriftssetningen, og denne gangen inneholder resultatet faktisk resultatet av søkeoperasjonen. Som forventet har den nye koden samme oppførsel som om den var synkron.
En annen mindre ting å huske på er at siden vi ikke lenger har tilbakeringingsfunksjoner, må vi håndtere searchOccurrences-feilen inne i samme funksjon siden vi ikke bare kan forplante feilen til tilbakeringingsfunksjonen og håndtere den der. Her skriver vi bare ut en feilmelding i tilfelle feil for eksempelets skyld.
Avslutning
I denne artikkelen gikk vi gjennom de forskjellige tilnærmingene som brukes for å implementere asynkron logikk i Javascript. Vi startet med å utforske et konkret eksempel på hvorfor vi måtte skifte fra den vanlige synkrone programmeringsstilen til den asynkrone modellen. Vi gikk deretter over til tilbakeringing, som er hovedbyggesteinene i asynkron Javascript. Begrensningene for tilbakeringinger førte oss til de forskjellige alternativene som ble lagt til gjennom årene for å overvinne disse begrensningene, hovedsakelig løfter og Async/avvent. Asynkron logikk kan bli funnet hvor som helst på nettet, enten du kaller et eksternt API, starter en databasespørring, skriver til det lokale filsystemet eller til og med venter på brukerinndata på et påloggingsskjema. Forhåpentligvis føler du deg nå mer trygg på å takle disse problemene ved å skrive rent og vedlikeholdbart asynkront Javascript!
Hvis du liker denne artikkelen, vennligst sjekk ut CLA-bloggen der vi diskuterer ulike emner om hvordan du kommer inn i teknologi. Sjekk også ut youtube-kanalen for våre tidligere gratis workshops og følg oss på sosiale medier slik at du ikke går glipp av kommende!