Jos olet vasta aloittamassa ohjelmointia, on mahdollista, että ajattelet ohjelmia sarjana peräkkäisiä logiikkalohkoja, joissa jokainen lohko tekee tietyn asian ja välittää tuloksensa, jotta seuraava lohko voidaan suorittaa ja niin edelleen, ja suurimmaksi osaksi olet oikeassa, useimmat ohjelmat toimivat peräkkäin, tämän mallin avulla voimme rakentaa ohjelmia, jotka on helppo kirjoittaa ja ylläpitää. On kuitenkin erityisiä käyttötapauksia, joissa tämä peräkkäinen malli ei toimi tai olisi optimaalinen. Harkitse esimerkiksi kirjanlukusovellusta. Tässä sovelluksessa on muutamia edistyneitä ominaisuuksia, kuten sanan kaikkien esiintymien etsiminen, kirjanmerkkien välillä liikkuminen ja vastaavat. Kuvittele nyt, että käyttäjä lukee parhaillaan pitkää kirjaa ja päättää etsiä kaikkia yleisen sanan, kuten "The", esiintymiä. Sovelluksella kestää yleensä muutama sekunti löytää ja indeksoida kaikki kyseisen sanan esiintymät. Jaksottaisessa ohjelmassa käyttäjä ei voi olla vuorovaikutuksessa sovelluksen kanssa (vaihtaa sivua tai korostaa tekstiä) ennen kuin hakutoiminto on suoritettu. Toivottavasti näet, että se ei ole optimaalinen käyttökokemus!
Kaavio havainnollistaa kirjanlukijasovelluksen tyypillistä suorituskulkua. Jos käyttäjä käynnistää pitkään käynnissä olevan toiminnon (tässä tapauksessa etsii kaikkia esiintymiä "the" suuresta kirjasta), sovellus "jäätyy" koko toiminnon ajaksi. Tässä tapauksessa käyttäjä jatkaa seuraavan kirjanmerkkipainikkeen napsauttamista ilman tulosta, kunnes haku on valmis, ja kaikki toiminnot tulevat voimaan kerralla antaen loppukäyttäjälle viivästyneen sovelluksen tunteen.
Olet ehkä huomannut, että tämä esimerkki ei todellakaan vastaa aiemmin esittelemäämme peräkkäistä mallia. Tämä johtuu siitä, että toiminnot ovat toisistaan riippumattomia. Käyttäjän ei tarvitse tietää "the":n esiintymisten lukumäärää siirtyäkseen seuraavaan kirjanmerkkiin, joten toimintojen suoritusjärjestys ei ole kovin tärkeä. Meidän ei tarvitse odottaa hakutoiminnon loppua ennen kuin voimme siirtyä seuraavaan kirjanmerkkiin. Mahdollinen parannus edelliseen suorituskulkuun perustuu tähän logiikkaan: voimme suorittaa pitkän hakuoperaation taustalla, jatkaa tulevien toimintojen kanssa ja kun pitkä toiminto on tehty, voimme yksinkertaisesti ilmoittaa käyttäjälle. Suorituskulku on seuraava:
Tämän suoritusvirran ansiosta käyttökokemus paranee merkittävästi. Nyt käyttäjä voi aloittaa pitkään käynnissä olevan toiminnon, jatkaa sovelluksen käyttöä normaalisti ja saada ilmoituksen, kun toiminto on suoritettu. Tämä on asynkronisen ohjelmoinnin perusta.
Javascript, muiden kielten ohella, tukee tätä asynkronisen ohjelmoinnin tyyliä tarjoamalla laajoja sovellusliittymiä, joiden avulla voit saavuttaa lähes minkä tahansa asynkronisen toiminnan. Loppujen lopuksi Javascriptin pitäisi olla luonnostaan asynkroninen kieli. Jos viitataan edelliseen esimerkkiin, asynkroninen logiikka on kaikkien käyttäjävuorovaikutussovellusten perusta, ja Javascript on ensisijaisesti rakennettu käytettäväksi selaimessa, jossa useimmat ohjelmat vastaavat käyttäjän toimiin.
Seuraavassa on lyhyt opas asynkronisesta Javascriptistä:
Takaisinsoittoja
Tyypillisessä ohjelmassa on yleensä useita toimintoja. Funktiota varten kutsumme sitä parametrijoukolla. Toimintokoodi suoritetaan ja palauttaa tuloksen, ei mitään poikkeavaa. Asynkroninen ohjelmointi muuttaa tätä logiikkaa hieman. Palataksemme kirjanlukijasovelluksen esimerkkiin, emme voi käyttää tavallista funktiota hakutoimintologiikan toteuttamiseen, koska toiminto vie tuntemattoman paljon aikaa. Tavallinen funktio palaa periaatteessa ennen toiminnon suorittamista, emmekä ole odottamamme käyttäytymistä. Ratkaisu on määrittää toinen toiminto, joka suoritetaan, kun hakutoiminto on suoritettu. Tämä mallintaa käyttötapaustamme, koska ohjelmamme voi jatkaa toimintaansa normaalisti ja kun hakutoiminto on suoritettu, määritetty toiminto ilmoittaa käyttäjälle hakutuloksista. Tätä toimintoa kutsumme takaisinsoittofunktioksi:
// 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);
Ensin määritellään hakutoimintotoiminto, searchOccurrences. Se vaatii etsittävän sanan ja toisen parametrin "takaisinkutsun", joka on toiminto, joka suoritetaan, kun hakutoiminto on suoritettu. Hakutoiminto pidettiin tarkoituksella abstraktina, meidän tarvitsee vain keskittyä sen kahteen mahdolliseen lopputulokseen: ensimmäisessä tapauksessa kaikki meni onnistuneesti ja meillä on haun tulos tulosmuuttujassa. Tässä tapauksessa meidän on vain kutsuttava takaisinsoittotoiminto seuraavilla parametreilla: ensimmäinen parametri on null eli virhettä ei ole tapahtunut, toinen parametri on haettu sana ja kolmas ja ehkä tärkein parametri kolmesta, on hakutoiminnon tulos.
Toinen tapaus on, kun tapahtuu virhe, tämä on myös tapaus, jossa hakutoiminto suoritetaan ja meidän on kutsuttava takaisinsoittotoiminto. Käytämme try and catch -lohkoa siepataksemme minkä tahansa virheen ja kutsumme vain takaisinsoittofunktion virheobjektin kanssa catch-lohkosta.
Sitten määritimme takaisinsoittotoiminnon, handleSearchOccurrences, pitäen sen logiikan melko yksinkertaisena. Kyse on vain viestin tulostamisesta konsoliin. Tarkistamme ensin "err"-parametrin nähdäksemme, onko päätoiminnossa tapahtunut virhe. Siinä tapauksessa ilmoitamme vain käyttäjälle, että hakutoiminto päättyi virheeseen. Jos virheitä ei ilmennyt, tulostamme viestin haun tuloksella.
Lopuksi kutsumme searchOccurrences-funktiota sanalla "the". Toiminto toimii nyt normaalisti estämättä pääohjelmaa ja kun haku on tehty, takaisinsoitto suoritetaan ja saamme tulosviestin joko hakutuloksen tai virheilmoituksen kera.
Tässä on tärkeää mainita, että meillä on pääsy vain pää- ja takaisinsoittofunktioiden tulosmuuttujaan. Jos yritämme jotain tällaista:
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);
tulostuksen tulos olisi määrittelemätön, koska ohjelmat eivät odota searchOccurrences-funktion suorittamista. Se siirtyy seuraavaan käskyyn, joka on print-lause, ennen kuin tulosmuuttuja määritetään pääfunktion sisällä. Tämän seurauksena tulostamme määrittämättömän tulosmuuttujan.
Joten tämän logiikan perusteella meidän tulisi säilyttää kaikki tulosmuuttujaa käyttävä koodi takaisinsoittofunktion sisällä. Tämä ei ehkä näytä nyt ongelmalta, mutta se voi nopeasti kärjistyä todelliseksi ongelmaksi. Kuvittele tapaus, jossa meillä on ketju asynkronisia toimintoja, joiden on suoritettava peräkkäin. Tyypillisessä takaisinsoittologiikassa toteutat jotain tämän kaltaista:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Muista, että jokaisella takaisinkutsulla on virheparametri ja jokainen virhe on käsiteltävä erikseen. Tämä tekee yllä olevasta monimutkaisesta koodista vieläkin monimutkaisempaa ja vaikeampaa ylläpitää. Toivottavasti Promises on täällä ratkaisemaan takaisinsoitto-helvetin ongelman, käsittelemme sen seuraavaksi.
Lupauksia
Lupaukset rakentuvat takaisinsoittojen päälle ja toimivat samalla tavalla. Ne otettiin käyttöön osana ES6:n ominaisuuksia ratkaisemaan muutamia räikeitä takaisinsoittoongelmia, kuten takaisinsoittohelvetti. Lupaukset tarjoavat omat toiminnot, jotka suoritetaan onnistuneen valmistumisen (resolve) ja virheiden tapahtuessa (hylkäämisen). Seuraavassa on esimerkki searchOccurrencesista, joka on toteutettu lupauksilla:
// 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`);
});
Käydään läpi tekemämme muutokset:
SearchOccurrences-funktio palauttaa lupauksen. Lupauksen sisällä noudatamme samaa logiikkaa: meillä on kaksi toimintoa ratkaista ja hylätä, jotka edustavat takaisinkutsujamme sen sijaan, että meillä olisi yksi takaisinsoittotoiminto, joka käsittelee sekä onnistuneen suorituksen että suorituksen, jossa on virheitä. Lupaukset erottavat nämä kaksi tulosta ja tarjoavat puhtaan syntaksin pääfunktiota kutsuttaessa. Ratkaisutoiminto on "kiinnitetty" päätoimintoon käyttämällä "sitten"-avainsanaa. Tässä määritetään vain ratkaisufunktion kaksi parametria ja tulostetaan hakutulos. Sama pätee hylkäystoimintoon, se voidaan kytkeä "catch"-avainsanalla. Toivottavasti voit arvostaa lupausten tarjoamia etuja koodin luettavuuden ja puhtauden suhteen. Jos keskustelet siitä edelleen, katso, kuinka voimme ratkaista takaisinsoitto-helvetin ongelman ketjuttamalla yhteen asynkroniset funktiot peräkkäin:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Asynk./Odota
Async/Await ovat uusin lisäys asynkroniseen Javascript-työkaluvyöhykkeeseen. ES8:n kanssa esitellyt ne tarjoavat uuden kerroksen abstraktiota asynkronisten toimintojen päälle yksinkertaisesti "odottamalla" asynkronisen toiminnon suorittamista. Ohjelman kulku lohkot kyseisessä käskyssä, kunnes tulos palautetaan asynkronisesta operaatiosta ja sitten ohjelma jatkaa seuraavalla käskyllä. Jos ajattelet synkronista suorituskulkua, olet oikeassa. Olemme tulleet täyteen ympyrään! Async/await yrittää tuoda synkronisen ohjelmoinnin yksinkertaisuuden asynkroniseen maailmaan. Muista, että tämä havaitaan vain ohjelman suorituksessa ja koodissa. Kaikki pysyy ennallaan konepellin alla, Async/await käyttää edelleen lupauksia ja takaisinsoitto on heidän rakennuspalikoitaan.
Käydään läpi esimerkkimme ja toteutetaan se Async/awaitilla:
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}`);
Koodimme ei juurikaan muuttunut, tässä on tärkeä huomioida "async"-avainsana ennen searchOccurrences-funktion ilmoitusta. Tämä osoittaa, että toiminto on asynkroninen. Huomaa myös "odottaa"-avainsana, kun kutsut searchOccurrences-toimintoa. Tämä ohjeistaa ohjelman odottamaan funktion suorittamista, kunnes tulos palautetaan ennen kuin ohjelma voi siirtyä seuraavaan käskyyn, toisin sanoen tulosmuuttuja sisältää aina searchOccurrences-funktion palautetun arvon eikä lupausta funktiolla, tässä mielessä Async/Awaitilla ei ole odotustilaa lupauksina. Kun suoritus on suoritettu, siirrytään print-lauseeseen ja tällä kertaa tulos sisältää itse asiassa hakutoiminnon tuloksen. Kuten odotettiin, uusi koodi käyttäytyy samalla tavalla kuin jos se olisi synkroninen.
Toinen pieni asia, joka on pidettävä mielessä, on, että koska meillä ei ole enää takaisinsoittotoimintoja, meidän on käsiteltävä searchOccurrences-virhe saman funktion sisällä, koska emme voi vain levittää virhettä takaisinsoittotoimintoon ja käsitellä sitä siellä. Tässä vain tulostamme virheilmoituksen virheen sattuessa esimerkin vuoksi.
Paketoida
Tässä artikkelissa kävimme läpi erilaisia lähestymistapoja, joita käytetään asynkronisen logiikan toteuttamiseen Javascriptissä. Aloitimme tutkimalla konkreettista esimerkkiä siitä, miksi meidän pitäisi siirtyä tavallisesta synkronisesta ohjelmointityylistä asynkroniseen malliin. Sitten siirryimme takaisinsoittoihin, jotka ovat asynkronisen Javascriptin päärakennuspalikoita. Takaisinsoittojen rajoitukset johtivat meidät eri vaihtoehtoihin, joita on lisätty vuosien varrella näiden rajoitusten voittamiseksi, pääasiassa lupauksiin ja Async/waitiin. Asynkroninen logiikka löytyy mistä tahansa verkosta, olitpa sitten kutsumassa ulkoista APIa, käynnistämässä tietokantakyselyä, kirjoittamassa paikalliseen tiedostojärjestelmään tai jopa odottamassa käyttäjän syötteitä kirjautumislomakkeella. Toivottavasti voit nyt itsevarmemmin ratkaista nämä ongelmat kirjoittamalla puhtaan ja ylläpidettävän asynkronisen Javascriptin!
Jos pidät tästä artikkelista, tutustu CLA-blogiin, jossa keskustelemme eri aiheista tekniikan alalle pääsemiseksi. Katso myös youtube-kanavaltamme aiemmat ilmaiset työpajamme ja seuraa meitä sosiaalinen media, jotta et jää paitsi tulevista!