Promises

ES6 udostępnia nam konstruktor Promises na bazie którego możemy tworzyć obiekty, które wykorzystamy do obsługi zadań asynchronicznych.

Wyobraź sobie, że masz funkcję asynchroniczną. Dla urealnienia przykładów skorzystamy z XMLHttpRequest:


function readData() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts');
    xhr.onload = function() {
        if (this.status === 200) {
            console.log(JSON.parse(this.responseText));
        }
    }
    xhr.send();
}
readData();

Chcielibyśmy teraz, aby tuż po zakończeniu działania readData() zaczęła działać nasza druga funkcja - np insertUsers(). Moglibyśmy to osiągnąć wywołując funkcję insertusers wewnątrz obiektu xhr:


function insertUsers(users) {
    users.forEach(user => {
        console.log(user);
    })
}

function readData() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts');
    xhr.onload = function() {
        if (this.status === 200) {
            insertUsers(JSON.parse(this.responseText))
        } else {
            console.error('Wystąpił błąd w ściąganiu danych');
        }
    }
    xhr.send();
}
readData();

Jak widzisz nie jest to najlepsze rozwiązanie. Trzeba szukać wewnątrz funkcji jakiś odwołań do innych funkcji. Dobrze, że powyżej mamy tylko jedno odwołanie. A co gdyby funkcja insertUsers() też była asynchroniczna a i odpalała by kolejną funkcję asynchroniczną? Są przypadki, że takich asynchronicznych funkcji chcielibyśmy odpalić kilka jedna po drugiej. W powyższym kodzie czekało by nas niezłe skakanie z miejsca na miejsce.

O wiele lepiej by było, gdybyśmy mogli w jakiś sposób obiecać reszcie funkcji, że jeżeli funkcja readData() się zakończy, wtedy je wykonamy...


readData()
    .then(...tutaj dalej działamy...)

I tutaj właśnie przychodzą nam z pomocą obiekty typu Promise - czyli obietnice.

Ogólna konstrukcja wywołania Promise ma postać:


const promise = new Promise(function(resolve, reject) {
    resolve(zwracana_poprawna_wartosc);
    reject(zwracana_bledna_wartosc);
});

Każdy promise może zwracać poprawne lub błędne rozwiązanie danej obietnicy. Powiedzmy, że nasz promise ma nam zwrócić dane z serwera. Jeżeli będą one poprawne, wtedy zwrócimy je za pomocą metody resolve(). Jeżeli w między czasie nastąpi jakiś błąd, albo dane będą błędne, możemy je zwrócić za pomocą reject():


const promise = new Promise(function(resolve, reject) {
    if (data.ok) {
        resolve(data.users);
    } else {
        reject(data.error);
    }
});

Poniżej bardziej praktyczny przykład - czyli pobieranie danych z serwera za pomocą XMLHTTPRequest:


const promise = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", 'http://example-serwer');
    xhr.addEventListener('load', () => resolve(xhr.responseText));
    xhr.addEventListener('error', () => reject(xhr.statusText));
    xhr.send();
});

Promise.then()

Po stworzeniu takiego promise możemy czekać na jago rozwiązanie (wykonanie). Służą do tego dodatkowe metody. Pierwszą z tych metod jest then(), która pozwala zareagować na rozwiązanie (resolve) promise:


const promise = new Promise(function(resolve, reject) {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", 'http://......');
    xhr.addEventListener('load', () => resolve(xhr.responseText));
    xhr.send();
});

promise.then(function(res) {
    //ten kod wykona się jeżeli powyższy promise zwróci resolve
    //w tym przypadku pod res trafi xhr.responseText czyli odpowiedź zapytania
});

Promise.catch()

Promise może zwracać odpowiedź pozytywną (resolve) lub negatywną (reject). Do obsługi negatywnej użyjemy metody catch():.


const promise = new Promise(function(resolve, reject) {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", 'http://......');
    xhr.addEventListener('load', () => resolve(xhr.responseText));
    xhr.addEventListener('error', () => reject(xhr.statusText));
    xhr.send();
});

promise.then(function(res) {
    //ten kod wykona się jeżeli powyższy promise zwróci resolve
    //pod res trafi data.users
}).catch(function(error) {
    //tutaj pod error trafi xhr.statusText czyli status
    //błędu który przekazaliśmy powyżej
});

Użycie w funkcji

Jeżeli teraz taki obiekt Promise zostanie zwrócony przez jakąś funkcję, możemy na takiej funkcji działać tak samo jak powyżej działaliśmy na promise:


function checkData() {
    return new Promise(function(resolve, reject) {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", 'http://......');
        xhr.addEventListener('load', () => resolve(xhr.responseText));
        xhr.addEventListener('error', () => reject(xhr.statusText));
        xhr.send();
    });
}

checkData().then(function(resp) {
    console.log(resp);
}).catch(function(err) {
    console.log(err);
})

Najlepiej uczyć się na przykładach, dlatego zmodyfikujmy nasz początkowy listing i zastosujmy w nim Promise:


function insertUsers(users) {
    users.forEach(user => {
        console.log(user);
    })
}

function readData() {
    return new Promise(function(resolve, reject) {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts');
        xhr.onload = function() {
            if (this.status === 200) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject('Wystąpił błąd w ściąganiu danych');
            }
        }
        xhr.send();
    })
}


readData().then(function(data) {
    insertUsers(data);
}).catch(function(err) {
    console.log(err);
});

Jak widzisz już nie musimy wywoływać funkcji z wnętrza asynchronicznych obiektów. W tej chwili nasze funkcje stały się autonomicznymi klockami, które możemy używać jak klasyczne funkcje. Dzięki temu nasz kod staje się o wiele czytelniejszy. W jednym pliku mamy funkcje, w drugim tylko ich używamy.

Promise.all()

Podobnych funkcji asynchronicznych jak powyższa readData() może być więcej - np. w przypadku gdy chcemy pobrać dane z kilku źródeł. Bardzo często resztę kodu chcielibyśmy wykonać dopiero po zakończeniu działania ich wszystkich. Aby poczekać na zakończenie wszystkich wskazanych promisów użyjemy metody Promise.all() do której przekażemy tablicę zawierającą nasze promisy:


function checkData1() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('OK1');
        }, 1000)
    });
}

function checkData2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('OK2');
        }, 2000)
    });
}

Promise.all([checkData1(), checkData2()])
    .then(function(resp) {
        console.log(resp); //["OK1", "OK2"]
        console.log(resp[0]); //OK1
        console.log(resp[1]); //OK2
    });

Promise.race()

Jeżeli powyższa metoda Promise.all() czekała na zakończenie wszystkich obietnic, tak metoda race() zwróci pierwszy zakończony promise:


function checkData1() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('OK1');
        }, 2000)
    });
}

function checkData2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('OK2');
        }, 1000)
    });
}

Promise.race([ checkData1(), checkData2() ])
    .then(function(resp) {
        console.log(resp); //OK2
    });

Łańcuchowe odpowiedzi

Jeżeli dana funkcja zwróci nowy promise, możemy na niej wykonać jedną z powyższych metod czyli then, catch itp.

Rozważmy powyższy przykład. Każda z funkcji zwraca nowy Promise, więc możemy na niej wykonać then. Jeżeli w takim then znowu zwrócimy Promise, możemy znowu wykonać kolejne then.


function checkData1() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve('OK1'); }, 2000);
    });
}

function checkData2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve('OK2'); }, 2000);
    });
}

function checkData3() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve('OK3'); }, 2000);
    });
}

checkData1()
    .then(res => checkData2())
    .then(res => checkData3())
    .then(function(resp) {
        console.log(resp); //OK3
    });

Co ciekawe samo then() zwraca nam promise, więc możemy je łączyć w łańcuchy bez potrzeby tworzenia specjalnych funkcji jak powyżej:


new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve(10);
    }, 2000);
})
.then(function(num) {
    console.log('first then: ', num);
    return num * 2;
})
.then(function(num) {
    console.log('second then: ', num);
    return num * 2;
})
.then(function(num) {
    console.log('last then: ', num);
});

Jeżeli któreś z powyższych funkcji then() zwróci normalną wartość (strng, numer, boolean itp), zostanie ta wartość potraktowana jako resolve. Jeżeli jednak któreś z tych then() wyrzuci taką wartość za pomocą throw, wtedy zostanie to potraktowane jako reject:


new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve(10);
    }, 2000);
})
.then(function(num) {
    console.log('first then: ', num);
    return num * 2;
})
.then(function(num) {
    console.log('second then: ', num);
    throw num * 2;
})
.then(function(num) {
    //to się nie wykona, bo powyżej został zwrócony reject
    console.log('last then: ', num);
})
.catch(function(error) {
    //to się wykona bo reagujemy na reject
    console.warn('Błąd', error);
})

W większości przypadków powyższe łączenie then będziesz stosował przy np. ściąganiu danych za pomocą fetch, które też zwraca promise:


fetch('....')
    .then(res => res.json())
    .then(res => {
        console.log(res);
    })