Obietnice - Promise

Ostatnia aktualizacja: 24 lutego 2021

Promise

Obietnice (Promise) pozwalają nam nieco inaczej podejść do pracy z asynchronicznymi operacjami.

Dzięki nim możemy wykonać jakiś kod, a następnie odpowiednio zareagować na jego wykonanie. Można powiedzieć, żę to taka inna odmiana funkcji callback.

Kiedy kupisz mi burgera, Wtedy będę zadowolony

Praca z obietnicami w zasadzie zawsze składa się z 2 kroków. Po pierwsze za pomocą konstruktora Promise tworzymy obietnicę. Po drugie za pomocą odpowiedniej funkcji reagujemy na jej zakończenie (konsumujemy ją). Zupełnie jak w powyższym zdaniu.

promise

Tworzenie Promise

Do stworzenia Promise korzystamy z konstruktora Promise(), który w parametrze przyjmuje funkcję, do której przekazujemy referencję do dwóch funkcji (tak zwanych egzekutorów), które będą wywoływane w przypadku zwrócenia poprawnego lub błędnego kodu.


const promise = new Promise((resolve, reject) => {
    if (zakończono_pozytywnie) {
        resolve("Wszystko ok 😁");
    } else {
        reject("Nie jest ok 😥");
    }
});

Każda obietnica może zakończyć się na dwa sposoby - powodzeniem (resolve) i niepowodzeniem (reject).
Gdy obietnica zakończy się powodzeniem (np. dane się wczytają), powinniśmy wywołać funkcję resolve(), przekazując do niej rezultat działania. W przypadku błędów powinniśmy wywołać funkcję reject(), do której przekażemy błędne dane lub komunikat błędu.

Po stworzeniu nowego obiektu Promise, w pierwszym momencie ma ona właściwości state (w debugerze widoczna jako [[PromiseStatus]] ustawioną na "pending" oraz właściwość value ([[PromiseValue]]), która początkowo wynosi undefined.

Promise pending

Gdzieś w tle dzieją się asynchroniczne operacje, które zajmują jakiś czas (np. trwa ściąganie danych).

W momencie zakończenia wykonywania takich operacji Promise przechodzi w stan "settled" (ustalony/załatwiony) i zostaje zwrócony jakiś wynik. Status takiego promise przełączany jest odpowiednio w "fulfilled" lub "rejected", a my jako programiści wywołujemy przekazane w parametrach funkcje (resolve lub reject) z przekazanymi do nich wynikami operacji.

Promise resolve reject

Konsumpcja Promise

Po rozwiązaniu (zakończeniu) Promise możemy zareagować na jego wynik. Służą do tego dodatkowe metody, które Promise nam udostępnia. Pierwszą z tych metoda jest then(). Pozwala ona reagować zarówno na pozytywne rozwiązanie obietnicy, negatywne jak i oba na raz:


const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Przykładowe dane");
    }, 1000);
});

promise.then(result => {
    //obietnica zakończyła się pozytywnie
    console.log(result)
});

function doThings() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Przykładowe dane");
        }, 1000);
    });
}

doThings()
    .then(res => {
        console.log(res)
    });

catch

Obietnica może zakończyć się pozytywnie (resolve) lub negatywnie (reject). Do reakcji na negatywną odpowiedź możemy albo drugiego parametru funkcji then(), albo metody catch() (stosowane częściej).


function doThings() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            //resolve("Gotowe dane");
            reject("Przykładowy błąd"); //uwaga zwracamy błąd
        }, 1000);
    });
}

loadData()
    .then(result => {
        ...
    })
    .catch(error => {
        console.error(error);
    });

Promise.all

Bardzo często nasze czynności chcielibyśmy zacząć wykonywać dopiero po zakończeniu wszystkich kilku asynchronicznych operacji. Przykładem takiej sytuacji może być widok użytkownika, na którym wyświetlamy jego dane, jego galerię, książki, posiadane zwierzęta (bo takie mieć musi...). Aby poczekać na zakończenie wszystkich wczytywań, czyli obietnic - użyjemy metody Promise.all() do której przekażemy tablicę zawierającą nasze obietnice:


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)
    });
}


Promise.all([
    loadUserData(),
    loadBooks(),
    loadPets()
])
.then(resp => {
    console.log(resp); //["A", "B", "C"]
    console.log(resp[0]); //"A"
    console.log(resp[1]); //"B"
    console.log(resp[2]); //"C"
});

Promise.allSettled

Podobną w działaniu jest funkcja allSettled().

Różnica w porównaniu z all() jest taka, że funkcja all() zwraca wynik, gdy wszystkie przekazane do niej promisy zostaną zakończone pozytywnie, natomiast allSettled() zwraca wynik gdy się zakończą - nie ważne czy pozytywnie czy negatywnie.


function checkData1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve("A"); }, 1000)
    });
}

function checkData2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { reject("B"); }, 1000) //REJECT!!!!
    });
}

Promise.all([
    checkData1(),
    checkData2()
])
.then(resp => {
    console.log(resp);
})
.catch(err => {
    console.log(err); //catch ponieważ jedna z funkcji zwróciła reject
})

/*
rezultat:
"B", ponieważ zadziałał catch, bo druga funkcja zwróciła reject
*/

function checkData1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve("A"); }, 1000)
    });
}

function checkData2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { reject("B"); }, 1000) //REJECT!!!!
    });
}

Promise.allSettled([
    checkData1(),
    checkData2()
])
.then(resp => {
    console.log(resp); //["A", "B"] then, bo zwracane są tutaj wszystkie
})
.catch(err => {
    console.log(err);
})

/*
rezultat:
[
    {status: "fulfilled", value: "A"}
    {status: "rejected", reason: "B"}
]
*/

Promise.race

Jeżeli powyższa metoda Promise.all() czekała na zakończenie wszystkich obietnic, tak metoda race() zwróci pierwszą zakończoną obietnicę:


function checkData1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve("A"); }, 2000)
    });
}

function checkData2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => { resolve("B"); }, 1000)
    });
}

Promise.race([
    checkData1(),
    checkData2()
])
.then(resp => {
    console.log(resp); //"B"
});

Promise.finally

Jeżeli interesowałeś się rozdziałem związanym z AJAX w jQuery, pewnie pamiętasz funkcja done(), fail() i always().

I tak funkcja done() odpalana jest po pozytywnym zakończeniu połączenia, metoda fail() odpalana jest w momencie błędu, a metoda always() odpalana jest po zakończeniu połączenia - nieważne czy zakończyło się pozytywnie czy negatywnie.

Porównując to do powyższych kodów, done() to taki nasz then(), fail() to catch(), ale gdzie podział się always()?

W wersji ECMAScript 2018 wprowadzono nową funkcję finally(), która jest odpowiednikiem właśnie funkcji always() w jQuerowym Ajax, co oznacza, że odpalana jest po zakończeniu Promise, bez względu czy zakończyło się powodzeniem czy nie.


btnLoadMore.classList.add("loading"); //pokazujemy loading
btnLoadMore.disabled = true; //i wyłączamy button

fetch("....")
    .then(res => res.json())
    .then(res => console.log(res))
    .catch(err => alert(err))
    .finally(() => {
        btnLoadMore.classList.remove("loading");
        btnLoadMore.disabled = false;
    });

Łańcuchowe obietnice

Jeżeli dana funkcja zwróci nam nową obietnicę, możemy na niej wykonać jedną z powyższych metod czyli then(), catch() itp.

Każda z takich funkcji także zwraca obietnicę, więc możemy wykonać kolejne operacje za pomocą kolejnych then():


const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("ok"), 1000);
});

promise
.then(res => {
    console.log(res); //"ok"
    return res + "2";
})
.then(res => {
    console.log(res); //"ok2"
    return res + "3";
})
.then(res => {
    console.log(res); //"ok23"
})

function checkDataA() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("OK1"), 2000);
    });
}

function checkDataB() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("OK2"), 2000);
    });
}

function checkDataC() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("OK3"), 2000);
    });
}

checkDataA()
    .then(res => checkDataB())
    .then(res => checkDataC())
    .then(res => {
        console.log(resp);
    });

//lub
checkDataA()
    .then(checkDataB)
    .then(checkDataC)
    .then(res => {
        console.log(res);
    });

Takie łańcuchowe wywoływanie kolejnych obietnic jest o tyle istotne, ponieważ bardzo często przy tworzeniu funkcji wczytujących dane niektóre funkcje then() będziemy wykonywać w samej funkcji, natomiast resztę poza jej ciałem reagując na zwróconą obietnicę:


function makeThings() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("obietnica, ");
        }, 2000);
    }).then(res => {
        return res + " pierwsza zmiana, ";
    }).then(res => {
        return res + " druga zmiana";
    })
}

makeThings()
    .then(res => {
        console.log("na zewnątrz: ", res);
    })

Przyda nam się to szczególnie, gdy będziemy pisać funkcje pobierające dane. Część obróbki danych wykonamy wewnątrz funkcji, natomiast cała reszta trafi poza funkcję.


function loadData(countryName) {
    return fetch(`https://restcountries.eu/rest/v2/name/${countryName}`) //fetch zwraca nam obietnicę
        .then(res => { //then też zwraca nam obietnicę
            if (res.ok) {
                return res.json();
            } else {
                return Promise.reject(`Http error: ${res.status}`);
            }
        })
}

loadData("Poland") //skoro loadData() zwróciło nam obietnicę, możemy na nią reagować
    .then(res => {
        console.log(res);
    })
    .catch(err => {
        console.error(err);
    })

Zauważ jak w powyższym kodzie w razie błędu kończymy negatywnie obietnicę. Nie możemy użyć parametrów Promise, bo takich nie mamy. Możemy natomiast użyć jednej z funkcji statycznych udostępnionych przez konstruktor Promise: Promise.resolve() i Promise.reject(). Ta pierwsza służy do pozytywnego zakończenia obietnicy, natomiast na druga do negatywnego.

Negatywne zakończenie możemy też uzyskać rzucając błędem:


function loadData(countryName) {
    return fetch(`https://restcountries.eu/rest/v2/name/${countryName}`) //fetch zwraca nam obietnicę
        .then(res => { //then też zwraca nam obietnicę
            if (res.ok) {
                return res.json();
            } else {
                throw new Error(`Http error: ${res.status}`);
            }
        })
}

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę z tego działu, to zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania-ajax

W repozytorium jest branch "solutions". Tam znajdziesz przykładowe rozwiązania.

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.

Menu