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 () => {
    function loadUserData() { ... }
    function loadBooks() { ... }
    function loadPets() { ... }

    async function render() {
        const user = await loadUserData();
        const books = await loadBooks();
        const pets = await loadPets();
        return data;
    }

    const r = await render();
})();
Przy czym w ES2022 zostanie to zmienione tak, byśmy mogli używać async nawet w głównym kodzie - nawet poza funkcjami (pkt 3).

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

Bardzo fajny film na powyższe tematy znajdziesz pod adresem: https://www.youtube.com/watch?v=vn3tm0quoqE

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.