Asynkroninen JavaScript¶
Attention
Asynkroninen JavaScript on laaja aihe, vaikeahkokin, mutta se on myös erittäin tärkeä aihe. Ilman asynkronista JavaScriptiä emme tekisi sinulla on mitä tahansa nykyaikaisia interaktiivisia verkkosivuja tai verkkosovelluksia.
Ota aikaa tutustuaksesi asynkronisen käsitteisiin JavaScript ja katso viitteet ja lisämateriaalit jokaisen lopussa osio alla.
Loput harjoitukset ja kurssitehtävä käyttävät asynkronista JavaScript.
JavaScript on yksisäikeinen https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Concepts#JavaScript_is_single_threaded vaikka asynkroninen ja ei-esto. Miten se saavutetaan?
Kun tehtävää käsitellään “pääsäikeessä”, mitään muuta ei voi tehdä samaan aikaan. Tarkastellaanpa joitain “estokoodeja”:
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
// Function execution stops here until user
// interacts with the alert
alert('You clicked me!');
// Code proceeds here once user has interacted with the alert
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});
Mitä tapahtuu, kun painiketta napsautetaan?
Luodaan hälytys. Mitään ei voida tehdä ennen kuin käyttäjä napsauttaa hälytystä.
Sivulle lisätään uusi kappale
Kun suoritat tehtävän, jonka suorittaminen vie jonkin aikaa, sinun tulee käyttää “työntekijäketjut” pääsäikeen sijaan. Tällä tavalla käyttäjä voi silti olla vuorovaikutuksessa sivulla, kun tehtävää käsitellään.
Referenssit ja lisämateriaali:
Takaisinsoittoja¶
Mitä nämä niin sanotut takaisinkutsut oikein ovat?¶
Takaisinsoitto on tapa viivyttää toiminnon suorittamista. Katsotaanpa seuraavaa koodiesimerkkiä:
function add(x, y) {
return x + y;
};
function addFive(x, referenceToAdd) {
return referenceToAdd(5, x);
};
addFive(5, add); // 10
Funktio “add” ottaa kaksi argumenttia “x” ja “y” ja palauttaa sitten niiden summan.
Funktio “addFive” ottaa kaksi argumenttia “x” ja “viittaus toiseen funktioon”.
Sitten se kutsuu funktiota, joka välitettiin arvoilla x
ja 5.
Funktio “addFive” on “korkeamman asteen funktio”, koska se ottaa funktion viittaus parametriksi.
Funktio “add” on “takaisinsoitto”, kun se välitetään “addFive”-funktiolle argumenttina.
function add(x, y) {
return x + y;
}
function higherOrderFunction(x, callback) {
return callback(5, x);
}
higherOrderFunction(5, add);
Mitä haittoja callbackien käytöstä on?¶
Tosielämän skenaarioissa teet usein paljon API-kutsuja tietojen hakemiseksi ja kuvia tietokannoista. Joskus sinun on esimerkiksi:
Hae käyttäjätiedot
Hae jotain muuta käyttäjätietojen perusteella
Hae jotain muuta vaiheen 2 tulosten perusteella.
Tämä ei kuulosta liian monimutkaiselta, mutta se alkaa karkaamaan hallinnasta melko nopeasti. Katsotaanpa esimerkkiä, jossa on vain vaiheet 1 ja 2. Ensin haemme käyttäjätiedot API:sta ja kun saamme ne hakea käyttäjien blogitekstejä. (Käytämme ilmaista testaussovellusliittymää https://jsonplaceholder.typicode.com joka ei sisällä oikeita käyttäjätietoja, mutta joitain väärennettyjä tietoja testaustarkoituksiin.)
const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;
document.getElementById("fetchBtn").addEventListener("click", () => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
document.getElementById("userData").innerText = xhr.responseText;
const xhr2 = new XMLHttpRequest();
xhr2.onreadystatechange = function() {
if (xhr2.readyState == 4) {
if (xhr2.status == 200) {
document.getElementById("postsData").innerText = xhr2.responseText;
} else {
console.error("Unsuccessful transfer (error code: " + xhr.statusText + ")");
}
}
}
xhr2.open("GET", `${postsUrl}?userId=${userId}`, true)
xhr2.send();
} else {
console.error("Unsuccessful transfer (error code: " + xhr2.statusText + ")");
}
}
}
xhr.open("GET", `${usersUrl}/${userId}`, true)
xhr.send();
})
Yllä oleva koodi käyttää XMLHttpRequest
tietojen hakemiseen. Sinun ei tarvitse tietää, miten se toimii
mutta yllä oleva koodi osoittaa, kuinka nopeasti takaisinsoitto voi riistäytyä hallinnasta ja
koodin sisennykset ja sisäkkäiset takaisinkutsut tekevät koodilogiikan seuraamisesta erittäin vaikeaa.
Kuvittele nyt, jos sinun pitäisi lisätä kolmas tai neljäs haku. Pyramid of DOOM tai mitä joskus kutsutaan myös Callback Helliksi alkaa vähitellen hahmottua.
Referenssit ja lisämateriaali:
Promises¶
Promises kehitettiin helpottamaan asynkronisen koodin kirjoittamista. Lupausobjekti edustaa asynkronisen toiminnan lopullista arvoa tai epäonnistumista.
Lupaus on arvo, jota ei välttämättä tunneta, kun se luodaan. Lupauksella on kolme eri tilaa:
Odottaa
Lupausta ei ole vielä luettu
Toteutunut
Lupaus onnistui
Arvo on käyttökelpoinen
Hylätty
Toiminto epäonnistui
Lupauskonstruktori ottaa yhden argumentin, takaisinkutsufunktion kahdella parametrilla, resolve ja reject, jotka ovat molemmat itse takaisinsoittoja.
const myFirstPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Success!");
}, 1000);
});
myFirstPromise.then((successMessage) => {
console.log("Yay! " + successMessage);
});
Voimme tehdä asynkronointitoimintoja Promisen sisällä. Kun olemme tehneet kaiken tarvitsemme, voimme kutsua “ratkaisemaan”. Jos jokin menee pieleen, meidän on soitettava hylkää.
Promise.prototype.then
ja Promise.prototype.catch
palauttavat lupaukset
jotta ne voidaan ketjuttaa.
Yllä olevassa esimerkissä luomme uuden “lupauksen”, joka ratkeaa odottamisen jälkeen 1000 ms:n ajan. Kun se suoritetaan, saamme ensin “lupauksen” odottavassa tilassa. Yhden sekunnin kuluttua “lupaus” “ratkeutuu” ja siirrymme “console.log” -tiedostoon.
Katsotaan seuraavaksi aiempaa esimerkkiä tietojen hakemisesta API:sta.
const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;
document.getElementById("fetchBtn").addEventListener("click", function() {
fetch(`${usersUrl}/${userId}`)
.then((response) => {
return response.json();
})
.then((user) => {
document.getElementById("userData").innerText = JSON.stringify(user);
return user;
})
.then((user) => {
fetch(`${postsUrl}?userId=${user.id}`)
.then((response) => {
return response.json();
})
.then((posts) => {
document.getElementById("postsData").innerText = JSON.stringify(posts);
})
})
.catch((error) => {
console.error(error.message);
})
})
Promiseilla koodista tulee hieman luettavampaa. Melko monet kirjastot tarjoavat mahdollisuus käyttää “Promiseja” async-toimintoihinsa. JavaScriptin haku on saatavana lähes kaikista ympäristöistä, joten se on hyvä valinta tietojen hakemiseen.
Referenssit ja lisämateriaali:
Harjoitus¶
Aiot rakentaa funktion myFirstPromise
, joka kestää shouldFail
parametrina. Tämä funktio palauttaa Promise
.
Jos
shouldFail
on true, Promisereject
palautusarvollaFail
.Jos “shouldFail” on “false”, lupaus “resolve” palauttaa arvon “Onnistuminen”.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Async/Await¶
Vaikka Promiset
ovat luettavampia kuin takaisinkutsut, ne eivät silti ole täydellisiä.
“Promise”-käsitteenä on hieman hankala haltuunotettava, joten monet aloittelevat JS-koodarit
kamppailevat sen kanssa.
Siksi syntaktista sokeria
on kehitetty piilottamaan Promiset ja asynkronisen kommunikoinnin monimutkaisuus. Tässä osiossa tutkimme
async
ja await
-avainsanoja.
Funktio voidaan julistaa “asynkroniseksi” käyttämällä “async”-avainsanaa:
async function example() {}
const example = async () => {};
Async-funktiot toimivat eri järjestyksessä kuin tavalliset toiminnot event-loopin kautta. Async-funktion palautusarvo on implisiittinen lupaus.
Async-funktion sisällä voimme käyttää `` odota`` avainsanaa. Odota keskeyttää funktion suorittamisen ja odottaa lupauksen ratkeamista.
Suoritetaan edellinen esimerkki, joka ratkaisee yhden sekunnin kuluttua “async”-avainsanalla:
function resolveAfter1Second() {
return new Promise(resolve => {
setTimeout(function() {
resolve("success!")
}, 1000)
})
}
const runAsync = async () => {
const response = await resolveAfter1Second();
console.log("The operation was " + response);
};
runAsync(); // The operation was success!
Voimme julistaa lupaukset täsmälleen kuten ennenkin ja sitten odottaa niiden toteutumista async-funktioissa.
Katsotaan seuraavaksi edellistä API-esimerkkiä. Voiko sitä parantaa käyttämällä “async/await”-komentoa?
const baseUrl = "https://jsonplaceholder.typicode.com";
const postsUrl = `${baseUrl}/posts`;
const usersUrl = `${baseUrl}/users`;
const userId = 1;
const fetchAsync = async url => {
const response = await fetch(url);
const data = await response.json();
return data;
}
document.getElementById("fetchBtn").addEventListener("click", async () => {
const userData = await fetchAsync(`${usersUrl}/${userId}`);
document.getElementById("userData").innerText = JSON.stringify(userData);
const userPosts = await fetchAsync(`${postsUrl}?userId=${userData.id}`);
document.getElementById("postsData").innerText = JSON.stringify(userPosts);
});
Vau, näyttää paljon siistimmältä!
Katsotaanpa ensin fetchAsync
-toimintoa. Muista edellisestämme
esimerkkejä, joista “fetch” palautti “Promisen” ja joita voimme “odottaa”.
“lupaus” ratkaistavaksi? Nyt voimme hakea tietoja mistä tahansa URL-osoitteesta kolmella rivillä
uudelleenkäytettävää koodia.
Koska määritämme fetchBtn
:n callback-funktio asynkroniseksi
voimme käyttää “await” siellä.
“Async/await” tekee koodista helpommin luettavan, nopeamman kirjoittaa ja intuitiivisemman testata.
Referenssit ja lisämateriaali:
Promise.all¶
Aiemmat esimerkimme ovat olleet tietojen peräkkäisestä hakemisesta:
Hae käyttäjä 1
Hae viestit käyttäjälle 1
Entä jos meidän pitäisi hakea tietoja käyttäjiltä 1, 2 ja 3? Mitä jos käyttäjiä olisi sata?
Promise.all sopii erinomaisesti tietojen hakemiseen rinnakkain. Se ottaa “iterable” argumenttina ja palauttaa “lupauksen”. Tämä “lupaus” täyttyy, kun kaikki on “iterable” on täytetty.
Toisin sanoen annat tyypillisesti joukon “lupauksia” kohteelle “Promise.all”. “Promise.all” täyttyy, kun jokainen “lupaus” lupausten joukossa on täyttynyt.
Promise.all() rejects, jos jokin taulukon lupauksista hylätään.
Muutetaan esimerkkiämme hieman. Sen sijaan, että noudettaisiin käyttäjä ja sitten tietoja kyseisen käyttäjän viesteistä, voisimme hakea kaksi käyttäjää ja näyttää tietonsa. Nämä pyynnöt voidaan tehdä samanaikaisesti Promise.all:n kanssa.
const baseUrl = "https://jsonplaceholder.typicode.com";
const usersUrl = `${baseUrl}/users`;
const usersArray = [1, 2];
const fetchAsync = async url => {
const response = await fetch(url);
const data = await response.json();
return data;
}
document.getElementById("fetchBtn").addEventListener("click", async () => {
// Notice how the map operation takes in an async function which always
// returns promises and usersData will therefore be an array of promises
const usersData = usersArray.map(async user => fetchAsync(`${usersUrl}/${user}`));
Promise.all(usersData).then(completed => {
completed.map((user, index) => {
document.getElementById(`userData${index + 1}`).innerText = JSON.stringify(user);
});
});
})
“usersData” on joukko “lupauksia”. Kun välitämme “usersData” argumenttina “Promise.all” saamme yhden “lupauksen”. Kun tuo yksittäinen “lupaus” ratkeaa siirrymme sitten-lauseeseen, jossa meillä on käytettävissä olevat tiedot.
Referenssit ja lisämateriaali:
Loppusanat ja lisää resursseja¶
Tämä luku esitteli monia varsin monimutkaisia käsitteitä lyhyessä ajassa.
JavaScriptin asynkroninen luonne hämmentää monia uusia verkkokehittäjiä, jotka kysyvät samat kysymykset StackOverflowssa yhä uudelleen ja uudelleen. Harjoitella tarpeeksi JavaScriptin asynkronista käyttöä heti alussa ja lue lisää siitä, niin koodauskokemuksesi on paljon parempi.
Katso viitteet ja linkit kunkin yllä olevan osion lopussa saadaksesi lisätietoja.