Computer Keyboard - Głównie JavaScript

Głównie JavaScript

by Jakub T. Jankiewicz

Asynchroniczność w JavaScript cz. 2: Async/Await

Drewniane, kolorowe, stylizowane kaczki ustawione w kolejce

Async/Await to dwa nowe słowa kluczowe dodane do języka JavaScript w wersji ES8 (inna nazwa to ES2017), które ułatwiają pisanie funkcji, które operują na obietnicach (ang. promises). Czyli służą do tworzenia funkcji asynchronicznych.

Jeśli nie jesteś zaznajomiony z obietnicami, możesz zobaczyć pierwszą część o programowaniu asynchronicznym w JavaScript.

Słowo kluczowe async, służy do oznaczania funkcji jako asynchronicznych, które będą operować na obietnicach. Obietnice natomiast dzięki słowu await staja się synchroniczne (a dokładniej ich wygląd w kodzie wygląda na sekwencyjny i synchroniczny). Można obietnicę przypisywać do zmiennych, wewnątrz których znajdą się wartości obietnic, dodając słowo kluczowe await przed obietnicą.

Poniżej przykładowy kod:

async function total(username) {
   var res = await fetch("/users/" + username);
   var user = await res.json();
   return user.total;
}

Użyto dwóch słów kluczowych await, ponieważ funkcja fetch, zwraca obietnicę zasobu, który udostępnia funkcje text() oraz json(), które po wywołaniu zwracają następną obietnicę.

Wynikiem funkcji async jest funkcja, która zwraca obietnicę, więc można jej użyć w ten sposób:

total('jan').then(function(total) {
   console.log('jan ma ' + total);
});

Czyli dokładnie tak jakby nasza funkcja była zapisana w taki, mniej czytelny sposób:

function total(username) {
   return fetch("/users/" + username)
        .then((res) => res.json())
        .then((user) => user.total);
}

Można oczywiście użyć, tej funkcji, razem ze słowem await, w innej funkcji async.

Tak naprawdę funkcja async, nie musi mieć w sobie słowa kluczowego await, a i tak będzie zwracała obietnicę, np:

async function getUsername(user) {
    return user.username;
}

var person = {
    username: 'Jan'
};

getUsername(person).then(function(username) {
    console.log(username);
});

Czyli jakby była zapisana w taki sposób:

function getUsername(user) {
    return Promise.resolve(user.username);
}

Zadziała także pusta funkcja. Po prostu obietnicą, będzie wartość undefined, ponieważ w języku JavaScript, każda funkcja, która nie zwraca jawnie wartości, będzie zwracała wartość undefined.

Jako bardziej rozbudowany przykład, przypomnijmy sobie kod z poprzedniej części o obietnicach:

getUsers().then(function(users) {
    return Promise.all(users.map(function(user) {
        return Promise.all([user, getProducts(user.id)]);
    }));
}).then(function(data) {
    return Promise.all(data.map(function([user, products]) {
        var total_promise = products.reduce(function(promise, product) {
            return promise.then(function(total) {
                return Promise.all([getPrice(product.id),
                                    getQuantity(user.id, product.id)])
                    .then(([price, count]) => total + (count * price));
            });
        }, Promise.resolve(0));
        return Promise.all([user, total_promise]);
    }));
}).then(function(data) {
    data.forEach(function([user, total]) {
        console.log(user.name + ' ' + total);
    });
}).catch(function() {
    console.log('Błąd w którejś obietnicy, nigdy się nie wywoła');
});

Mamy tu tablice użytkowników, a każdy z nich ma tablicę produktów. Z kolei każdy produkt, ma cenę oraz liczność. Każda z tych wartości jest pobierana poprzez funkcję, która zwraca obietnicę. Powyższy kod, jako funkcja async, wyglądałby tak:

async function displayUsersTotal() {
    try {
        var users = await getUsers();
        for (let user of users) {
            user.products = await getProducts(user.id);
            for (let product of user.products) {
                product.price = await getPrice(product.id)
                product.count = await getQuantity(user.id, product.id);
            }
            user.total = user.products.reduce(function(acc, product) {
                return acc + (product.price * product.count);
            }, 0);
            console.log(user.name + ' ' + user.total);
        }
    } catch (e) {
        console.log('Błąd w którejś obietnicy, nigdy się nie wywoła');
    }
}

Jak widzicie kod jest o wiele krótszy i o wiele bardziej czytelny. W powyższym kodzie, oprócz async/await, użyłem słowa kluczowego let oraz pętli for..of. Let działa tak jak var, tylko że zasięg zmiennej znajduję się wewnątrz bloku, w którym została zdefiniowana, czyli poza instrukcja for, będzie niezdefiniowana oraz każda iteracja pętli będzie miała swoją zmienną. Natomiast pętla for..of to nowy dodatek do języka JavaScript, dzięki któremu iterując po tablicy, iterujemy po jej wartościach, a nie tak jak w przypadku for..in po kluczach/indeksach.

Dla porównania kod z funkcjami zwrotnymi, z pierwszej części:

getUsers(function(users) {
    users.forEach(function(user) {
        getProducts(user.id, function(products) {
            var total = 0;
            products.forEach(function(product, i) {
                getPrice(product.id, function(price) {
                    product.price = price;
                    getQuantity(user.id, product.id, function(quantity) {
                        total += product.price * quantity;
                        if (products.length - 1 == i) {
                            console.log(user.name + ' ' + total);
                        }
                    });
                });
            });
        });
    });
});

Jeśli funkcja, która wywoływana jest ze słowem kluczowym await, się nie powiedzie (wywoła się funkcja reject obietnicy), wyrzucony zostanie zwykły wyjątek. Dlatego wystarczy jedna instrukcja try..catch, aby je przechwycić. Tak jak w przypadku samych obietnic wystarczy jedno miejsce obsługi błędów. Wszystkie wyjątki wyrzucane wewnątrz funkcji async są konwertowane na odrzucaną obietnicę.

Przykład:

function rejected() {
    return new Promise((_, reject) => reject('rejected'));
}

async function foo() {
    try {
        await rejected();
    } catch (e) {
        console.log('error');
        throw e;
    }
}

foo().then(() => console.log('nigdy się nie wyświetli')).catch((e) => console.log(e));

Powyższy kod wyświetli ’error’ i ’rejected’.

Słowo kluczowe await, może być wywoływane, tylko i wyłącznie wewnątrz funkcja async. Ale istnieje propozycja aby dodać możliwość użycia go bez async. Z chwilą pisania tego artykułu, wiem tylko o jednej możliwości użycia await poza funkcja async. Można go użyć w konsoli devtools przeglądarki Google Chrome/Chromium. Dlatego aby skorzystać z await „luzem”, trzeba utworzyć IIFE (ang. Immediately Invoked Function Expression), czyli funkcję anonimową, którą od razu wywołujemy:

(async function() {
   var username = 'jan';
   var res = await fetch('/users/' + username);
   var user = await res.json();
   console.log(user.fullName);
})();

Można też użyć funkcji strzałkowej:

(async () => {
   var username = 'jan';
   var res = await fetch('/users/' + username);
   var user = await res.json();
   console.log(user.fullName);
})();

Async/Await obsługuje większość nowoczesnych przeglądarek (oprócz oczywiście IE). Ich listę możesz zobaczyć na can I use.

źródło strony (aby zobaczyć kod na GitHubie, musisz kliknąć przycisk raw)
Komentarze

Hasło, które podasz umożliwi ponowne zalogowanie się i np. usunięcie komentarza, jest dobrowolne. Email jest szyfrowany i używany do wysyłania powiadomień o odpowiedziach do komentarzy oraz do pobierania awatara dzięki usłudze Gravatar.com.