Промисы (Promises) в JavaScript

Владислав Белецкий
Владислав Белецкий .
Категория:
Комментариев: 0

Промисы (Promises), или Обещания, в JavaScript появились в стандарте EcmaScript2015 (ES6) и с тех пор надежно обосновались в нем для обработки асинхронных операций.

Разработчики дали промисам такое название неслучайно. Promise переводится с английского, как обещание, и как любое обещание может быть либо выполнен, либо не выполнен – и третьего не дано. Для отслеживания этих двух результатов у него есть 2 соответствующие функции – resolve() и reject() и 3 состояния:

  • ожидание (pending): начальное состояние, не исполнен и не отклонен.
  • исполнено (fulfilled): операция завершена успешно.
  • отклонено (rejected): операция завершена с ошибкой.

Т.е. при создании промис сначала находится в режиме ожидания, пока не выполнится некий записанный вами код, а затем переходит только в одно из возможных состояний: fulfilled – при успешном завершении вашего кода или rejected – при неуспешном его выполнении (например, ошибке соединения с сервером). После этого вы передаете управление кодом 2-м дополнительным функциям, которые каким-либо образом обрабатывают успех или неудачу вашего кода. При успешном результате вызывается функция then(), ошибка же обрабатывается в функции catch().

promise-resolve-reject

Если перевести then()  с английского (затем, потом, тогда), то можно сказать, что данный метод что-то делает после того, как отработает функция resolve(), т.е. он потом отслеживает успешное выполнение некоего задания. Метод catch() переводится, как ловить или поймать, и нужен, чтоб отловить ошибку из функции reject(), аналогично тому, как это выполняется в конструкции try…catch().

Давайте посмотрим на поддержку браузерами объекта Promise. Сайт caniuse.com выдает нам такую информацию:

Т.е. за исключением Internet Explorer и Opera Mini, которые традиционно мало что из нововведений поддерживают, мы можем использовать промисы в большинстве современных браузеров.

Разберемся с этой технологией подробнее.

Пример со случайным числом на основе промисов

Рассмотрим пример, в котором будем имитировать игру “Случайный номер”. С помощью математической функции Math.random() будем получать случайный номер в диапазоне от 0 до 400, но демонстрировать пользователю результат будем с некоторой задержкой, имитируя либо соединение с сервером, либо “задумчивость” генератора случайных чисел.

Мы используем функции resolve() и reject(), предоставляемые промисами по умолчанию, для того чтобы отследить, в какой диапазон попало наше случайное число, и в зависимости от этого выведем сообщение с помощью функции then() и catch().

Код примера таков:

Протестируйте пример сами, нажав на кнопку несколько раз.

    var output = document.getElementById(‘output’);

    function randomNum(x) {
    return Math.round(Math.random() * x);
    }

    function testPromise() {

    let numPromise = new Promise(function(resolve, reject) {
    let num = randomNum(400);
    setTimeout(function() {
    if (num > 200) resolve(‘Отлично! Вы выиграли! Ваш номер ‘ + num);
    else reject(‘Извините, но вы проиграли. Ваш номер ‘ + num);
    }, 500)
    console.log(num);

    });
    numPromise.then(mes => output.insertAdjacentHTML(‘beforeend’, ‘

  1. ‘ + mes + ”))
    .catch(mes => output.insertAdjacentHTML(‘beforeend’, ‘

  2. ‘ + mes + ”))
    }
    if (“Promise” in window) {
    let btn = document.getElementById(“btn”);
    btn.addEventListener(“click”, testPromise);
    } else {
    output.innerHTML = ‘К сожалению, ваш браузер не поддерживает интерфейс Promise.';
    }

    Этот пример в IE выдаст нам то предупреждение, которое мы установили при проверке:

    promis-ы в Internet Explorer

    Пример с подсчетом чисел с помощью Promise

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

    Теперь вызов функции resolve() или reject() зависит от того, как пользователь решил пример, а then() и catch() выводят соответствующие сообщения об успешном или неуспешном решении.

    Обратите внимание, что функция reject() принимает один параметр, но он может быть объектом, в котором вы можете передать 2 или более параметров, задав им названия.

    Давайте протестируем пример:

    #testUserNum {
    margin: 15px 0;
    padding: 10px;
    border: 4px double #ccc;
    }

    // let randNumber = (min, max) => Math.round(Math.random()*(max-min)+min);
    function doTestUser(){
    let testUserPromise = new Promise((resolve, reject) => {
    let num1 = randomNum(100), num2 = randomNum(150);
    let userNum = parseInt(prompt(`Введите число, равное ${num1} + ${num2}`));
    if(userNum === num1+num2) resolve(`Вы совершенно правы! ${num1} + ${num2} = ${num1+num2} `);
    else reject({message: `Ваш ответ ${num1} + ${num2} = ${userNum} не совпадает с верным результатом ${num1+num2}`, border: 'red' })
    })
    //вывод результата
    const testUserNum = document.getElementById('testUserNum');
    testUserPromise.then( message => {
    testUserNum.style.borderColor = '';
    testUserNum.innerHTML = message;
    }).catch(error => {
    testUserNum.style.borderColor = error.border;
    testUserNum.innerHTML = error.message;
    })
    }

    Отложенный вызов функций

    На самом деле никто не мешал нам при решении поставленных задач использовать самую обычную функцию и вывести пользователю сообщение об успехе или неудаче в условной конструкции if...else. Однако этот пример демонстрирует сам подход к разветвлению успешного и неуспешного выполнения какого-то действия внутри промиса.

    Как правило, промисы нужны там, где действие будет выполняться с некоторой задержкой, т.е. после какого-то периода ожидания. Такой период мы можем создать самостоятельно, используя метод setTimeout() при вызове любой функции. Например, мы последовательно выводим такой код:

    И именно в таком порядке он появляется в консоли.

    Последовательный вывод в консоль

    Однако, стоит добавить setTimeout() с разными значениями задержек - и последовательность вывода меняется в зависимости от значения этих задержек. Этот метод позволяет показать, что действия в JavaScript могут выполнятся асинхронно и непоследовательно.

    setTimeout в консоли

    На скриншоте хорошо видно,что строчки 1 и 4 выполнились практически одновременно, хотя разделены двумя другими строками кода. Кроме того, 3-я строка с периодом задержки в 1 секунду отобразилась после второй строки с задержкой в 2 секунды. Эта последовательность очень хорошо отображает процессы, которые обычно сопровождают отправку или загрузку данных по сети. Здесь имеется в виду то, что на эти процессы нужно время, причем оно увеличивается, если у вас медленный Интернет, например, где-нибудь за городом.

    Эмуляция загрузки данных с помощью метода setTimeout()

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

    Последовательный вызов функций

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

    See the Pen
    Step 1. Post without callback by Elen (@ambassador)
    on CodePen.0

    Вы ведь не смогли увидеть 3-й пост? Ничего удивительного. Задержка в 2 секунды привела к тому, что 3-й пост был добавлен в массив posts через секунду после того, как все посты уже были выведены на экран, поэтому мы его не увидим никогда.

    Использование функции-коллбека

    На втором шаге "полечим" предыдущий код, задав функцию-коллбек, которая будет вызвана сразу после добавления элемента в массив методом push(), а не в основном потоке.

    See the Pen Step 2. Post with callback by Elen (@ambassador)on CodePen.0

    Теперь все посты на месте, не так ли?

    Используем промисы для обработки задержки во времени

    В примере ниже мы уже будем использовать промис для отображения нового поста или для вывода сообщения об ошибке. Пока что ошибка у нас зависит от переменной error, которая генерируется, как случайное число в виде 0 или 1, что соответствует логическим false и true в JavaScript. Поэтому вы или увидите через время публикацию 3-го поста, или сообщение об ошибке. Нажмите на кнопку Rerun внизу, чтобы несколько раз запустить код.

    See the Pen Step 3. Posts with Promice by Elen (@ambassador)on CodePen.0

    Реальная загрузка данных с удаленного сервера

    Теперь рассмотрим пример загрузки JSON-файла с сервера JSONPlaceholder, пусть и с фейковым контентом, зато в формате JSON, который на данный момент является наиболее популярным для передачи данных. Использовать будем технологию Fetch API, в основе которой лежат промисы. Именно поэтому в коде мы также используем then() для отслеживания загрузки данных. Кроме того, метод .then() нам понадобится еще для того, чтоб использовать затем метод .json(), который возвращает нам массив объектов в синтаксисе JavaScript, а не в виде строк JSON-формата.

    See the Pen Step 3. Posts with Fetch API by Elen (@ambassador)on CodePen.0

    Ошибки при загрузке данных

    Когда мы загружаем файлы с сервера, он присылает заголовки, которые показывают, найден ли запрашиваемый файл или нет.  В этом случае в объекте Response, который мы получаем в результате вызова метода fetch(), будут находится сведения об этом в виде свойства ok со значением true или false, а также свойство status, содержащее цифру ответа сервера: 200 в случае успешной загрузки данных, 404 - в случае, если файл не найден, 500 в случае недоступности сервера и т.д. (почитать подробнее).

    fetch-запрос и ответ сервера с ошибкой и безВ примере ниже мы будем случайным образом генерировать пустую строку или слово 'no', которое при добавлении к URL-адресу запроса будет нам давать ошибку 404 (файл не найден). Мы же эту ошибку будем обрабатывать с помощью ключевого слова throw и выводить в диалоговом окне alert() внутри блока catch().

    Кстати, обратите внимание, что в этом запросе мы используем данные в виде параметра _start=3&_limit=10, которые позволяют загрузить с JSONPlaceholder данные с определенного номера и в определенном количестве.

    See the Pen Step 3_1. Fetch Mistakes by Elen (@ambassador) on CodePen.0

    Используем Fetch API с async/await функцией

    Возможно, вам не очень понравилось писать .then() каждый раз, когда нам надо сделать следующий шаг при работе с промисом, чтобы дождаться выполнения очередной операции. Вы не одиноки в своем стремлении получить более читабельный и краткий код. Для этого в ядро JavaScript в стандарте ES7 были добавлены функции с клбючевым словом async, которые нужны для отслеживания операций, выполняемых асинхронно. При этом подразумевается, что в этих функциях нам придется выполнять некие действия, которые требуют некоторого времени, и, соответственно, подразумевают, что мы ожидаем, когда процесс получения информации или отправки данных закончится. Поэтому здесь используется синтаксис с ключевым словом await.

    Например, мы дожидаемся загрузки данных методом fetch() поэтому, пишем

    Такой синтаксис даст нам возможность записать в переменную response ответ от сервера а в переменную posts - результат работы метода json() в виде массива объектов:

    Результаты использования await для загрузки данных с помощью fetch()Пример для тестирования:

    See the Pen Step 5. Posts from JSONPlaceholder with Fetch API and async/await functions by Elen (@ambassador) on CodePen.0

    Методы объекта Promise

    • Promise.all(iterable) - Ожидает исполнения всех промисов или отклонения любого из них. Возвращает промис, который исполнится после исполнения всех промисов в iterable. В случае, если любой из промисов будет отклонен, Promise.all будет также отклонен.

    let p1 = Promise.resolve("This is first Promise");
    let p2 = 100+25;
    let p3 = new Promise((resolve, reject) => setTimeout(resolve, 2000, "The end"));
    Promise.all([p1, p2, p3]).then( values => testAll.innerHTML = `

    ${values.join(' | ')}`)

    Рассмотрим еще один пример, когда мы вызываем несколько раз функцию, которая делает запрос на загрузку JSON-файла и возвращает нам массив объектов из этого файла. Используем метод Promise.all() для того чтобы  вывести полученные массивы объектов-постов от разных пользователей:

    See the Pen Step 6. Posts from JSONPlaceholder with Promise.all() by Elen (@ambassador)on CodePen.0

    • Promise.allSettled(iterable) - Ожидает завершения всех полученных промисов (как исполнения так и отклонения). Возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.
    • Promise.race(iterable) - Ожидает исполнения или отклонения любого из полученных промисов. Возвращает промис, который будет исполнен или отклонен с результатом исполнения первого исполненного или отклонённого промиса из iterable.
    • Promise.reject(reason) Возвращает промис, отклонённый из-за reason.
    • Promise.resolve(value)Возвращает промис, исполненный с результатом value.
  3. Подписаться
    Уведомить о
    guest
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии