Computer Keyboard - Głównie JavaScript

Głównie JavaScript

by Jakub T. Jankiewicz

Asynchroniczność cz. 4: Funkcja async jako generator

Piłka do golfa na trawie
Autor Mabel Amber, źródło pexels.com licencja CC0

Funkcje async oraz słowo kluczowe await są częścią es8 (es2017). Nie są dostępne we wszystkich przeglądarkach, chociaż ich wsparcie jest bardzo duże. Jeśli jesteś zainteresowany w jaki sposób Babel konwertuje async..await, aby przeglądarki, które ich nie obsługują mogły uruchomić ten kod, to ten wpis jest dla Ciebie. Kod ten wygląda jak jeden do jeden dlatego pomyślałem, że warto o tym napisać.

Generatory to część es6 czyli es2015 i właśnie za ich pomocą Babel tworzy funkcje, które w oryginale używają słowa kluczowego async. Udostępnia też kod tzw. regenerator dla kodu, który korzysta z generatorów (jeśli nie zmusimy go aby tego nie robił), ale my nie będziemy się nim tutaj zajmować.

Poniżej prosta funkcja asynchroniczna, korzystająca z async..await, która zwraca tytuł pliku RSS, dla strony Głównie JavaScript (blog używa CORS, więc można użyć tego kodu też na innych stronach np. z CodePen, link na końcu):

async function title() {
    var res = await fetch('https://jcubic.pl/feed.xml');
    var text = await res.text();
    var parser = new DOMParser();
    var xmlDoc = parser.parseFromString(text, "text/xml");
    return xmlDoc.querySelector('title').innerHTML;
}

Ta sama funkcja jako generator wygląda tak:

function* title() {
    var res = yield fetch('https://jcubic.pl/feed.xml');
    var text = yield res.text();
    var parser = new DOMParser();
    var xmlDoc = parser.parseFromString(text, "text/xml");
    return xmlDoc.querySelector('title').innerHTML;
}

Jedyna różnica to gwiazdka po słowie function zamiast słowa async oraz słowo yield zamiast await.

Aby ten kod zadziałał, możemy wykorzystać ciekawą właściwość iteratorów (czyli niskopoziomowego api, które kryje są za generatorami), a mianowicie możemy przekazać wartość, do następnego wywołania funkcji next iteratora i ta wartość zostanie zwrócona przez słowo kluczowe yield, kiedy iterator wznowi swoje działanie.

Funkcja która przetworzy generator i zwróci obietnice wartości, jaką zwraca oryginalna funkcja, wygląda tak:

// funkcja zwraca true dla zwykłych obietnic jak i obiektów,
// które wyglądają jak obietnice, jak np. jQuery Defered
function is_promise(value) {
    return value instanceof Promise || (typeof value === 'object' && typeof value.then === 'function');
}

function unwind(gen) {
    // pobieramy iterator z generatora
    var iterator = gen()[Symbol.iterator]();
    return new Promise(function(resolve) {
        (function next(value) {
            // przekazujemy poprzednią wartość do następnego next
            value = iterator.next(value);
            if (!value.done) {
                if (is_promise(value.value)) {
                    // wartość value z funkcji next to będzie już wartość obietnicy,
                    // a nie sama obietnica
                    value.value.then(next);
                } else {
                    // zwykła wartość - raczej nie użyjemy async dla takich wartości
                    // dlatego nie powinno wystąpić ale nic nie stoi na przeszkodzie
                    // aby wywołac var answer = await 42;
                    next(value.value);
                }
            } else {
                // nasza obietnica dostanie wartość, która zostaje zwrócona
                // przez return oryginalnej funkcji
                resolve(value.value);
            }
        })();
    });
}

A tutaj jak wywołać funkcje unwind wraz z generatorem:

unwind(title).then((title) => console.log(title));

Na koniec moje demko na CodePen, które zawiera kod z tego wpisu.

ź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.