Async / await
Async/await to nowy sposób na zapis asynchronicznego kodu. Dzięki tym słowom praca z obietnicami zaczyna przypominać synchroniczny kod.
Async
Słowo async postawione przed dowolną funkcją tworzy z niej funkcję asynchroniczną, która zwraca obietnicę. Dzięki temu możemy później na nią reagować poznanymi w poprzednim rozdziale funkcjami:
function doThings() {
return Promise.resolve("ok");
}
doThings()
.then(res => {
console.log(res);
})
//to samo co
async function doThings() {
return "ok";
}
doThings()
.then(res => {
console.log(res); //"ok"
});
Await
Słowo kluczowe await sprawia, że JavaScript poczeka na wykonanie asynchronicznego kodu. Dzięki temu zapis bardzo przypomina synchroniczny kod:
function loadUserData() {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve("A"); }, 1000)
});
}
function loadBooks() {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve("B"); }, 1000)
});
}
function loadPets() {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve("C"); }, 1000)
});
}
async function render() {
const user = await loadUserData();
const books = await loadBooks();
const pets = await loadPets();
return {user, books, pets}
}
Słowa await możemy używać tylko wewnątrz funkcji poprzedzonej słowem async:
function renderPage() {
const city = await render(); // SyntaxError: await is a reserved word
}
async function renderPage() {
const city = await render(); //ok
}
Niektórzy programiści wiedząc, że będą używać sporo asynchronicznych operacji wewnątrz swojego kodu, cały kod okrywają IIFE z async lub zdarzeniem DOMContentLoaded:
(async () => {
...
})();
document.addEventListener("DOMContentLoaded", async () => {
...
});
(async () => {
async function loadUserData() { ... }
async function loadBooks() { ... }
async function loadPets() { ... }
async function render() {
const user = await loadUserData();
const books = await loadBooks();
const pets = await loadPets();
return data;
}
const r = await render();
})();
Obsługa błędów
W przypadku obsługi błędów za pomocą async/await, możemy posłużyć się konstrukcją try/catch
, wewnątrz której spróbujemy wykonać jakieś operacje. Jeżeli one się nie powiodą przejmiemy komunikat błędu w sekcji catch:
(async () => {
//próbuję wykonać jakieś operacje
try {
const request = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const json = await request.json();
console.log(json)
//jeżeli się nie uda, zwracam błąd
} catch(err) {
console.log(err);
}
})();
function loadImage(src) {
const img = new Image();
img.src = src;
return new Promise((resolve, reject) => {
img.onload = function() {
resolve(img);
}
img.onerror = function() {
reject("błąd wczytywania");
}
if (img.complete) resolve(img);
});
}
(async () => {
try {
const imgA = await loadImage("przykladowyA.jpg");
const imgB = await loadImage("przykladowyB.jpg");
console.log(imgA);
console.log(imgB);
} catch(error) {
console.error(error);
}
})();
Możemy też mieszać składnię async/await z then/catch:
function loadImage(src) {
const img = new Image();
img.src = src;
return new Promise((resolve, reject) => {
img.onload = function() {
resolve(img);
}
img.onerror = function() {
reject("błąd wczytywania");
}
if (img.complete) resolve(img);
});
}
async function loadData() {
try {
const imgA = await loadImage("przykladowyA.jpg");
const imgB = await loadImage("przykladowyB.jpg");
console.log(imgA);
console.log(imgB);
} catch(error) {
//throw new Error(`error`);
//lub
return Promise.reject(`error`);
} finally {
console.log("Kończymy wczytywanie");
}
}
loadData()
.then(response => {
console.log(response);
})
.catch(error => {
console.log("Wystąpił błąd połączenia: ", error);
});
(async () => {
function loadImage(src) {
const img = new Image();
img.src = src;
return new Promise((resolve, reject) => {
img.onload = function() {
resolve(img);
}
img.onerror = function() {
reject("błąd wczytywania");
}
if (img.complete) resolve(img);
});
}
async function loadData() {
try {
const imgA = await loadImage("przykladowyA.jpg");
const imgB = await loadImage("przykladowyB.jpg");
console.log(imgA);
console.log(imgB);
} catch (error) {
return Promise.reject(error)
}
}
try {
const data = await loadData();
console.log(data);
} catch(error) {
console.error(error);
} finally {
console.log("We do cleanup here");
}
})();
Równoczesne operacje
Instrukcja await oznacza to, że kolejna operacja rozpocznie się dopiero, gdy poprzednia się zakończy.
W wielu momentach bardziej optymalnie będzie, gdy nasze operacje wywołamy równocześnie i poczekamy na ich zakończenie:
async function renderPage() {
const country = getCountry();
const weather = getWeather(country.lat, country.lng);
const countryData = await country;
const weatherData = await weather;
updatePage(countryData, weatherData);
}
W przypadku zapisu za pomocą Promise kod mógłby wyglądać tak:
function renderPage() {
const country = getCountry();
const weather = getWeather(country.lat, country.lng);
return Promise.all([country, weather])
}
renderPage().then(...)
Mały przykład
Bardzo często omawiając asynchroniczność, pierwsze co nam przychodzi do głowy to pobieranie czy wysyłanie danych. Ale przecież operacje zajmujące jakiś czas wcale nie muszą wiązać się z przesyłaniem danych.
Podsumujmy ten rozdział mini przykładem. Mamy klasę, która tworzy i za pomocą setInterval animuje wykres:
class Bar {
constructor({place, timeMs = 1000, color}) {
if (place === undefined) throw Error("Nie podano miejsca");
this.place = place;
this.timeMs = timeMs;
this.color = color || `hsl(${Math.random()*360}, 100%, 50%)`;
this.create();
}
create() {
this.bar = document.createElement("div");
this.bar.classList.add("bar");
this.progress = document.createElement("div");
this.progress.classList.add("progress");
this.progress.style.backgroundColor = this.color;
this.span = document.createElement("span");
this.span.innerHTML = `0%`;
this.bar.append(this.progress);
this.progress.append(this.span);
this.place.append(this.bar);
}
animate() {
let i = 0;
let time = setInterval(() => {
this.progress.style.width = `${++i}%`;
this.span.innerHTML = `${i}%`;
if (i >= 100) {
clearInterval(time)
}
}, this.timeMs / 100);
}
}
Na jej bazie tworzymy pięć wykresów w jakimś testowym elemencie:
const test = document.querySelector("#test");
const bar1 = new Bar({place: test});
const bar2 = new Bar({place: test});
const bar3 = new Bar({place: test});
const bar4 = new Bar({place: test});
const bar5 = new Bar({place: test});
bar1.animate();
bar2.animate();
bar3.animate();
bar4.animate();
bar5.animate();
Po odpaleniu powyższego kodu dostaniemy 5 wykresów, które równocześnie zaczną się zapełniać.
Chcielibyśmy, by kolejne wykresy zaczęły się zapełniać jeden po drugim.
Funkcja animująca trwa jakiś czas, więc musimy jakoś zareagować na jej zakończenie.
Moglibyśmy więc jak w poprzednim rozdziale zrobić to za pomocą funkcji zwrotnej:
class Bar {
...
animate(cb) {
let i = 0;
let time = setInterval(() => {
this.progress.style.width = `${++i}%`;
this.span.innerHTML = `${i}%`;
if (i >= 100) {
clearInterval(time)
cb();
}
}, this.timeMs / 100);
}
}
Wtedy reagowanie na zakończenie animacji zmusiło by nas do zagnieżdżania kolejnych wywołań.
const bar1 = new Bar({place: test});
const bar2 = new Bar({place: test});
const bar3 = new Bar({place: test});
const bar4 = new Bar({place: test});
const bar5 = new Bar({place: test});
bar1.animate(() => {
bar2.animate(() => {
bar3.animate(() => {
bar4.animate(() => {
bar5.animate(() => {
alert("Zakończono"); //tu znowu przydał by się Hadouken...
})
})
})
})
});
Możemy więc pokusić się o zamianę funkcji animate()
na obietnicę, dzięki czemu jej późniejsze użycie się uprości:
class Bar {
...
animate() {
return new Promise((resolve, reject) => {
let i = 0;
let time = setInterval(() => {
this.progress.style.width = `${++i}%`;
this.span.innerHTML = `${i}%`;
if (i >= 100) {
clearInterval(time)
resolve();
}
}, this.timeMs / 100);
});
}
}
const bar1 = new Bar({place: test});
const bar2 = new Bar({place: test});
const bar3 = new Bar({place: test});
const bar4 = new Bar({place: test});
const bar5 = new Bar({place: test});
bar1.animate()
.then(bar2.animate)
.then(bar3.animate)
.then(bar4.animate)
.then(bar5.animate)
.then(() => {
alert("Zakończono");
});
Ale możemy też połączyć to ze składnią async/await, co jeszcze bardziej uprości nasz kod:
(async () => {
const bar1 = new Bar({place: test});
const bar2 = new Bar({place: test});
const bar3 = new Bar({place: test});
const bar4 = new Bar({place: test});
const bar5 = new Bar({place: test});
await bar1.animate()
await bar2.animate()
await bar3.animate()
await bar4.animate()
await bar5.animate()
alert("Zakończono");
})()
Dodatkowe materiały
Trening czyni mistrza
Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania