Promise, fetch API

Что такое promise

Promise - Это объект с помощью которого мы работаем с отложенными и асинхронными операциями. Этот объект используется в качестве заполнителя для будущего результата асинхронной операциии. Или еще проще, promise просто контейнер для будущего значения. С помощью промисов мы работает с ajax. Нам пришел ответ, от вызова ajax, этот ответ мы и поместим в promise.

Создаем promise

Для создания промиса используем конструктор new Promise. Promise принимает в себя функцию с двумя аргументами.

  1. resolve - эта функция выполнится при удачном выполнении промиса.
  2. reject - эта функция выполнится при неудачном выполнении промиса. При работе с fetch мы подробнее разберем этот момент.
let b = new Promise((resolve,reject)=> {})

Promise может находится в трех состояниях:

  1. Состояние pending: начальное состояни. Ожидание, того когда станет доступно будущее значение.
  2. Состояние fulfilled: операция завершена удачно.
  3. Состояние rejected: операция завершена с ошибкой.
  • Есть такое понятие потребление promise, это когда мы работаем с промис и его состояниями. То есть мы создаем промис и потом потребляем. В работе с fetch мы будем только потреблять.

then

Для управления этими состояниями существует метод then, мы вызываем его прямо у промиса, он доступен для всех промисов. then в себя принимает колбек функцию, которая запустится когда промис перейдет в состояние выполнено(fulfilled).

let b = new Promise((resolve,reject)=> {
    return resolve('hi') // мы в промисе вернули resolve, а вунтри текст hi. промис выполнился
}).then(hi => console.log(hi)) // hi
// внутри then выполнилась колбек функция которая в свой аргумент приняла то, что вернул промис

catch

Метод catch будет выполняться при неудачном выполнении кода.

let b = new Promise((resolve,reject)=> {
    return resolve('hi')
}).then(hi => bye).catch(()=> { // тут я вернул не hi, а bye
    alert('Привет тут ошибка') // произошла ошибка и выполнился catch
}) 

finally

finally используется всегда в самом конце после всех then. Работает и при удачном выполнении и при неудачном. Например мы можем отчистить все данные после работы промиса и работы с сервером.

let b = new Promise((resolve,reject)=> {
    return resolve('hi')
}).then(hi => hi).finally(()=> {
    console.log('конец'); // конец
})

Далее расмотрим какие проблемы решает promise, а после как вообще с ним работать и для чего.

Callback hell

Колбек ад, это когда колбеков очень много, все становится очень запутанно и непонятно.

Пример для примера

// имитируем асинхронную работу
console.log('Запрос данных...'); // выводим запрос

setTimeout(()=> {
    console.log('Подготовка данных...'); // через 2 сек у нас идет подготовка

    const product = { // подготавливаем объект
        name:'TV',
        price: 3000
    }
    console.log(product); // выводим объект
    setTimeout(()=> {
        product.status = 'order'; // добавляем свойство в объект
        console.log(product.status); // order типо заказан
        setTimeout(()=> {
            product.sending = 'sent' // после типо мы отправили и бла бла бла
            console.log(product.sending); // sent
        },2000) // суть в том, что получается вот такая лесенка, это классический колбек ад
    },2000) // если нам нужна будет еще одна операция, у нас появится еще одна такая лесенка
},2000) // представим что их штук 5, вот тут и начинается ад

Что бы у нас наш код не превратился в колбек ад, нам понадабятся promise.

const rq = new Promise((resolve, reject) => { // создаем промис
    console.log('Запрос данных');
    setTimeout(() => {
        const product = {
            name: 'TV',
            price: 2000,
            bool: true
        };
        // получается так, что если все удачно прошло и мы получили наш объект product, то выполняется resolve, которая и принимает  в себя этот продукт
        resolve(product);
    }, 3000);
});
rq.then((product) => { // rq - это промис который выполнил свою работу, далее это переходит в then
    console.log('Подготовка данных');
    return new Promise((resolve, reject) => { // здесь мы можем вернуть еще один промис
        setTimeout(() => {
            product.status = 'order'; // модифицировать наш продукт
            if (product.bool === true) {
                resolve(product); // если у нас все четко и в bool: true, то работаем дальше
            } else { // иначе ошибка и используем reject
                reject();
                // само собой, это пример и никто не будет писать такое условие. Подробнее далее в разборе fetch
            }

        }, 2000)
    });
}).then((data) => { // тут опять получаем наш product
    data.modify = true; // меняем наш объект
    return data; // возвращаем его
}).then((data) => { 
    console.log(data);
}).catch(() => { 
    console.log('ОШИБКА');
}).finally(() => {
console.log('Конец');
});

По сути работа с промисами, превращается в такую вот некую цепочку промисов, при работе с fetch все станет намного яснее.

Пример с XMLHttpRequest и API

Сейчас мы воспользуемся API restcountries - с помощью него мы сможем получать информацию о странах, ну и делать с ними всякое, мм..ага..дада..

const getCountry = (country) => { // Создаем функцию, в нее будем передавать нужную страну

    const request = new XMLHttpRequest();  // создаем объект XMLHttpRequest

    // говорим, что нам надо, тут же указываем аргументом страну которую хотим
    request.open("GET", `https://restcountries.com/v3.1/name/${country}`); 
    request.send() // отправляем запрос
    request.addEventListener('load', () => {  // создаем событие load, оно будет срабатывать когда данные будут получены
        const data = JSON.parse(request.response); // парсим наш ответ в объект
        // у каждой страны язык находится в объекте languages, но имя свойств у всех стран разные
        // поэтому мы получаем просто их значения без ключей и все. Будем выводить самое первое значение
        const lang = Object.values(data[0].languages)
        
        // создадим переменную с html кодом, который поместим на страницу с версткой
        const html = `
        <div class="country"> 
        <img class="country-img" src="${data[0].flags.png}" />
        <div class="country-data">
          <h3 class="country-name">${data[0].name.official}</h3>
          <p class="country-lang"><span>Язык: </span>${lang[0]}</p>
        </div>
      </div>
        `
    document.querySelector('body').insertAdjacentHTML('afterbegin',html) // помещаем, про этот метод я говорил уже, да да
    
    
    })
}

getCountry('usa') // вызываем

Вот что мы получаем:

usa

А теперь посмотрим на колбек ад, который может произойти на практике.

XMLHttpRequest & callback hell

const showCountry = (data,className) => { // отдельно помещаем отображение страны
    const lang = Object.values(data[0].languages)
    const html = `
        <div class="country ${className}">
        <img class="country__img" src="${data[0].flags.png}" />
        <div class="country__data">
          <h3 class="country__name">${data[0].name.official}</h3>
          <p class="country__row"><span>Язык: </span>${lang[0]}</p>
        </div>
      </div>
        `
    document.querySelector('body').insertAdjacentHTML('afterbegin', html)
}


const getCountyAndBorderCountries = (country) => { // теперь мы получаем страну и ее первую соседнюю страну

    const request = new XMLHttpRequest();

    request.open("GET", `https://restcountries.com/v3.1/name/${country}`); // все так же как и было
    request.send()
    request.addEventListener('load', () => {
        const data = JSON.parse(request.response);
        showCountry(data) // теперь тут отображаем страну
        const [firstCountry] = data[0].borders; // получаем соседнюю страну
        const request2 = new XMLHttpRequest();  // вызываем новый ajax
        request2.open("GET", `https://restcountries.com/v3.1/alpha/${firstCountry}`); // теперь запрашиваем соседнюю страну
        request2.send() // все как всегда
        request2.addEventListener('load', () => { // событие load
            const data = JSON.parse(request2.response);
            showCountry(data,"neighbour") // и показываем ее с нужным классом

        })

    })
}

getCountyAndBorderCountries('usa')

Вот что получается:

usaAndCan

Вот получается у нас, что в одном ajax запросе находится еще один, теперь представим, что нам нужно еще узнать соседнюю страну соседней страны, или еще сделать какой то вызовы ajax. Вот тогда и будет ад, в одном запросе другой и так далее.
Теперь посмотрим как оно будет с fetch.

fetch API

fetch - это современный способ для работы с сетевыми запросами. Сейчас перенесем пример выше на fetch.

  • При работе с fetch не нужно делать промис в ручную, функция fetch создает промис и возвращает его.

Потребляем промис из fetch

Давайте потребим наш промис из функции fetch.

// кусочек из кода выше
const showCountry = (data,className = "") => {
    const lang = Object.values(data[0].languages)
    const html = `
        <div class="country ${className}">
        <img class="country__img" src="${data[0].flags.png}" />
        <div class="country__data">
          <h3 class="country__name">${data[0].name.official}</h3>
          <p class="country__row"><span>Язык: </span>${lang[0]}</p>
        </div>
      </div>
        `
    document.querySelector('body').insertAdjacentHTML('afterbegin', html)
}

// используя fetch получим нашу страну
const getCountryData = (country) => {
    // Мы просто вызываем fetch и помещаем api
    const response = fetch(`https://restcountries.com/v3.1/name/${country}`) // получаем ответ
    .then(data => data.json())  // если ответ есть, мы переходим в then, тут получаем данные объектом с помощью json()
    .then(data => showCountry(data)) // вызываем функцию показать страну.
}

getCountryData('usa') // вот и все.
  • Метод json() позволяет декодировать ответ в обычный объект.

Теперь давайте получим и соседа, исправим как раз колбек ад, который у нас начал появляться с XMLHttpRequest.

const getCountryData = (country) => {
    const response = fetch(`https://restcountries.com/v3.1/name/${country}`) // получаем ответ
        .then(data => data.json())  // если ответ есть, мы переходим в then, тут получаем данные объектом с помощью json()
        .then(data => {
            showCountry(data) // внутри then так же отображаем основную страну
            const [firstCountry] = data[0].borders; // получаем соседа

        // тут важно вернуть промис. Получаем соседа
        return fetch(`https://restcountries.com/v3.1/alpha/${firstCountry}`)}) 
        .then(data => data.json()) // тут мы продолжаем работать с промисом
        .then(data => showCountry(data, 'neighbour')) // тут только с нужным классом
}

getCountryData('usa')

Очень важный момент:

        // в конце получая вторую страну  мы могли бы написать так
          fetch(`https://restcountries.com/v3.1/alpha/${firstCountry}`)
            .then(data => data.json()) 
            .then(data => showCountry(data, 'neighbour'))
        // но это большая ошибка, таким образом у нас опять получится callback hell
        //  у нас одна колбек функция определена внутри другой колбек функции, внешней, то есть первым вызывом нашей главной страны
            // получается начиная опять внутри какой то вызов, у нас будет колбек ад

Теперь еще раз посмотирм на правильный вариант, так станет понятнее.

.then(data => {
            showCountry(data) 
            const [firstCountry] = data[0].borders; /

            return fetch(`https://restcountries.com/v3.1/alpha/${firstCountry}`) }) // тут заканчивается then который возвращает нам промис
          .then(data => data.json())  // и далее идут другие then
          .then(data => showCountry(data, 'neighbour'))  // мы не вызываем их внутри еще одного fetch, мы вызываем их лишь после другого then
          // таким образом у нас нет колбек ада

В общем и все, получаем тот же результат:

usaAndCan

Отклоненный promise

До этого мы конечно все сделали красиво, но мы не обработали никаких ошибок, а если сервер не ответит? Если ничего не придет? Давайте исправим и это. Будем использовать уже знакомые catch. В каком случае мы можем не получить наш промис?.Скорее всего, если у нас не будет интернета. В инспекторе кода на вкладке сеть & network можно открыть такое окошко:

network

Для отлова ошибки о дисконекте используем кнопку, при нажатии который мы будем получать наши страны.

const btn = document.querySelector('.btn'); // обычная кнопка с классом btn
btn.addEventListener('click',() => { // получаем страны при нажатии
    getCountryData('usa');
    
} )

После мы открываем страницу, где будет одна кнопка, в инспекторе кода в сети ставим офлайн и нажимаем кнопку, таким образом мы получим 2 ошибки.

errorNet

Если у вас появляются страны даже в офлайн режиме, то почистите кеш браузера. А после, рядом где ставили офлайн режим поставьте галочку "Отключить кеш".

Вспомним про reject.

Давайте теперь для отлова ошибки используем вторую колбек функцию, которая в обычном промисе называется reject. Называть ее конечно же, можно как угодно.

// начало кода
const getCountryData = (country) => { 
    const response = fetch(`https://restcountries.com/v3.1/name/${country}`)// получаем страну
        .then(data => data.json(), error => { // и тут добавляем вторую функцию
          console.log(error); // она выведет нам саму ошибку
        })  
}

Таким образом, если мы нажмем на нашу кнопку, для показа стран, то вторая ошибка, которая говорит, что мы не нашли промис, попадет во вторую нашу функцию и выведет ее в alert. Но если нужно отловить ошибку для следущего вызова, то придется опять писать функцию. Поэтому давайте вспомним про метод catch.

Вспомним про метод catch

Повторим функцию getCountryData.

const getCountryData = (country) => {
    const response = fetch(`https://restcountries.com/v3.1/name/${country}`)
        .then(data => data.json())  
        .then(data => {
            showCountry(data)
            const [firstCountry] = data[0].borders;

            return fetch(`https://restcountries.com/v3.1/alpha/${firstCountry}`) })
            .then(data => data.json()) 
            .then(data => showCountry(data, 'neighbour'))
          .catch((error)=> { // В конце ставим catch 
            alert(error) // и так же выводим нашу ошибку.
        })
}

Калькулятор валюты

А тут мы повторим тот же калькулятор, что и в примере с XMLHttpRequest:

const inputRub = document.querySelector('#rub'), // получаем элементы
    inputUsd = document.querySelector('#usd');

inputRub.addEventListener('input', async () => {  // чешаем событие на инпут
  let response = await fetch('./js/current.json', { // получаем ответ в переменную response
        method: "GET", // это тоже можно не указывать
        headers: { // вообще можно написать в одну строчку без method, headers.
            'Content-type': 'application/json; charset=utf-8'
        },
    })

    // свойство ok Будет true если status в диапозоне от 200 до 299
    if(response.ok) { 
        let data = await response.json() // парсим наш ответ в объект
        inputUsd.value = (+inputRub.value / data.current.usd).toFixed(2) // работаем с инпутами как и было до этого.
    } else {
        alert(`Ошибка запроса ${response.status}`) // Окно об ошибке
    }
})

Вот и все. Потыкать сам калькулятор можно тут . Можете все повторить из предыдущей темы и потом просто вставить этот код, все будет работать так же.

02.12.2022