JavaScript Promise с примерами

JavaScript Promise с примерами
Содержание:
  1. Что такое promise?
  2. Создание промиса
  3. Состояния, в которых может находиться промис
  4. Методы промисов
  5. Цепочка промисов
  6. Promice.all() и Promise.race()
  7. Задачи
  8. Комментарии

В этой статье разберём что такое промисы, как они работают, зачем они нужны, а также различные примеры их использования.

Что такое promise?

По умолчанию код в JavaScript выполняется последовательно (в одном потоке, синхронно). То есть таким образом, когда каждая следующая операция ждёт завершения предыдущей.

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

Чтобы этого избежать необходимо использовать асинхронный код. Он в отличие от синхронного выполняется в фоновом режиме и не блокирует основной поток. Т.е. код, расположенный после него выполняется сразу же, не дожидаясь его завершения.

Асинхронный код в JavaScript может быть написан разными способами: с помощью обратных вызовов, promise (обещаний) и ключевых слов async/await.

В этой статье рассмотрим, как это сделать с помощью promise.

Promise (промисы, обещания) – это специальный объект и набор методов в JavaScript для удобного написания асинхронного кода.

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

Изучение промисов начнём с рассмотрения примера из реальной жизни.

Допустим, папа обещает дать 100$, если вы сдадите завтрашний экзамен на хорошо или отлично.

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

Но как только вы получите отметку по экзамену, обещание завершится. Далее в зависимости от того успешно оно завершилось или нет будет зависеть получите ли вы 100$ или нет.

На JavaScript этот пример с помощью promise реализуется следующим образом:

JavaScript
// создадим новый промис
const promise = new Promise((resolve, reject) => {
  // оценку, которые мы получим определим случайным образом спустя некоторое время (например, 5 секунд)
  setTimeout(() => {
    // сгенерируем оценку от 2 до 5
    const mark = Math.floor(Math.random() * 4) + 2;
    // если оценка больше 3, то...
    if (mark > 3) {
      // завершим промис успешно: для этого вызовем функцию resolve() и передадим ей в скобках полученную оценку (это нужно для того, чтобы мы затем её могли получить в методе then())
      resolve(mark);
    }
    // завершим промис с ошибкой
    reject(mark);
  }, 5000);
});

// выполнение действий после завершения промиса выполняется с помощью методов: then (в случае успеха) и catch (при ошибке)
promise
  .then(result => console.log(`Ура! Я сдал экзамен на ${result}! Папа, как и обещал дал мне 100$.`))
  .catch(result => console.log(`Увы, я получил оценку ${result}! Папа мне не дал 100$`));

А теперь разберём как всё это работает, и начнём с создания промиса.

Создание промиса

Начинается процесс написания промиса с его создания. Осуществляется это с помощью конструктора, т.е. с new Promise():

JavaScript
const promise = new Promise((resolve, reject) => {
  // асинхронный код
});

Конструктор промиса принимает 2 аргумента, которые являются функциями. Первый аргумент обычно называют resolve, а второй – reject. Внутрь промиса помещают асинхронный код, можно конечно и синхронный, но тогда в этом не будет смысла.

Промис завершает своё выполнение, когда вызывается функция resolve() или reject().

Функцию resolve() вызывают обычно в том месте кода, в котором асинхронная операция должна завершиться успешно. А функцию reject() – там, где она должна завершиться с ошибкой.

Состояния, в которых может находиться промис

Промис начинается с состояния ожидания (state: "pending"). Оно говорит о том, что он ещё не выполнен (результат undefined).

Промис завершается после вызова resolve() или reject(). При этом его состояние переходит соответственно в выполнено (state: "fulfilled") или отклонено (state: "rejected").

Состояния, в которых может находиться промис

Внутрь функций resolve() или reject() можно поместить аргумент, который затем будет доступен соответственно в then() или catch().

JavaScript
// сдал ли экзамен
const passexam = true;
// промис
const result = new Promise((resolve, reject) => {
  setTimeout(() => {
    passexam ? resolve('Папа подарил 100$.') : reject('Папа не подарил 100$.');
  }, 5000);
});

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

Когда passexam равен true, промис успешно завершится через 5 секунд посредством вызова функции resolve(). А так как промис завершился успешно, то будет вызван метод then().

Состояние промиса при успешном его  завершении

Если значение константы passexam поменять на false, то промис завершится с ошибкой через 5 секунд с помощью вызова функции reject(). А так как в этом случае промис завершился с ошибкой, то, следовательно, будет вызван catch().

Состояние промиса при завершении с ошибкой

Методы промисов

У каждого промиса есть определённый набор методов, которые мы можем использовать:

  • then – выполняется, когда промис завершился успешно (после вызова функции resolve());
  • catch – вызывается, если промис завершается ошибкой (после вызова reject());
  • finally – выполняется в любом случае после завершения промиса, вне зависимости от конечного состояния.
JavaScript
?/ promise - промис
promise
  .then((value) => { // ... })
  .catch((error) => { // ... })
  .finally(() => { // ... });

Пример с использованием всех трёх методов:

JavaScript
<div><button id="run">Новая попытка</button></div>
<div id="result"></div>

<script>
  let isProcess = false;
  elResult = document.querySelector('#result');

  document.querySelector('#run').onclick = () => {
    if (isProcess) {
      elResult.textContent = 'Подождите! Задача ещё выполняется!';
      return;
    }
    isProcess = true;
    elResult.textContent = 'Задача в процессе...';
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        const mark = Math.floor(Math.random() * 4) + 2;
        mark > 3 ? resolve(mark) : reject(mark);
      }, 5000);
    });
    promise
      .then(value => {
        elResult.textContent = `Ура! Вы сдали экзамен на ${value}! Папа, как и обещал дал вам 100$.`;
      })
      .catch(value => {
        elResult.textContent = `Увы, вы получили оценку ${value}! Папа не дал вам 100$`;
      })
      .finally(() => {
        isProcess = false;
      });
  }
</script>

При этом then() позволяет обработать не только успех, но и ошибку. Для этого необходимо передать функцию в качестве второго аргумента.

JavaScript
// promise – промис
promise.then(
  value => { // действия при успешном завершении промиса  },
  error => { // действия при завершении промиса с ошибкой }
);

Второй аргумент указывать не обязательно, в этом случае он будет реагировать только на успех:

JavaScript
// promise – промис
promise.then(value => {
  // действия при успешном завершении промиса
});

Для того чтобы получить значение из промиса в методах then() и catch() как уже было отмечено выше необходимо его передать в функции resolve() и reject().

Цепочка промисов

Цепочка промисов – это простая концепция, в основу которой положено то, что методы промисов тоже в качестве результата возвращают промис. А так как они возвращают промис, то мы для них в свою очередь тоже можем вызвать методы и т.д. Таким образом, мы можем создавать цепочку из промисов, каждый из которых выполняется только после того как завершился предыдущий.

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

Пример цепочки промисов:

JavaScript
const promise = new Promise(resolve => {
  setTimeout(() => resolve(2), 3000);
});

promise
  .then(value => {
    console.log(value);
    return new Promise(resolve => {
      setTimeout(() => resolve(value * 2), 3000);
    });
  })
  .then(value => {
    console.log(value);
    return new Promise(resolve => {
      setTimeout(() => resolve(value * 2), 3000);
    });
  })
  .then(value => console.log(value))

В этом примере:

  • сначала выполняется первый промис, который успешно завершится через 3 секунды со значением 2;
  • после этого выполнится метод then(), который выведет в консоль значение переменной value, которое равно аргументу, переданному в resolve() и возвратит в качестве результата новый промис; он также как и предыдущий успешно завершится через 3 секунды со значением value * 2;
  • затем выполнится следующий then(), он выполнит действия аналогично предыдущему then();
  • дальше выполнится последний then(), который просто выведет в консоль значение параметра value.

Promice.all() и Promise.race()

Promice.all() и Promise.race() – это статические методы Promice, которые принимают на вход массив промисов и возвращают новый промис.

В случае с Promice.all() промис завершится когда завершатся все промисы в массиве.

JavaScript
const promise1 = new Promise(resolve => setTimeout(resolve, 1000, 'one'));
const promise2 = new Promise(resolve => setTimeout(resolve, 2000, 'two'));
const promise3 = new Promise(resolve => setTimeout(resolve, 3000, 'three'));

Promise.all([promise1, promise2, promise3]).then(value => console.log(value)); // ['one', 'two', 'three']

При использовании Promise.race() промис завершится, как только выполнится первый промис из массива.

JavaScript
const promise1 = new Promise(resolve => setTimeout(resolve, 1000, 'one'));
const promise2 = new Promise(resolve => setTimeout(resolve, 2000, 'two'));
const promise3 = new Promise(resolve => setTimeout(resolve, 3000, 'three'));

Promise.race([promise1, promise2, promise3]).then(value => console.log(value)); // 'one'

Задачи

1. Асинхронный код, ниже реализованный с использованием функций обратного вызова, необходимо переписать через промисы:

JavaScript
<button type="button" id="get-text">Получить текст с сервера</button>
<div id="result"></div>

<script>
  const request = (url, success, error) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.onload = () => {
      xhr.status === 200 ? success(xhr.response) : error(xhr.statusText);
    }
    xhr.onerror = () => error(xhr.statusText);
    xhr.send();
  }
  document.querySelector('#get-text').onclick = () => {
    request('/examples/ajax/01.html', data => {
      document.querySelector('#result').innerHTML = data;
    }, error => {
      console.error(error);
    });
  }
</script>

Решение

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

JavaScript
// функция, которую нужно реализовать
const sleep = ms => { ... }

// её использование для выполнения действия с задержкой 5 секунд
sleep(5000).then(() => {
  console.log('Выполнилось через 5 секунд!');
});
// её использование для выполнения действия с задержкой 10 секунд
sleep(10000).then(() => {
  console.log('Выполнилось через 10 секунд!');
});

Решение

3. Перепишите код для динамической загрузки скриптов в определенном порядке через промисы:

JavaScript
const loadJs = (src, success, error) => {
  const script = document.createElement('script');
  script.onload = success;
  script.onerror = error;
  script.src = src;
  document.head.append(script);
}

// код для открытия модального окна нужно выполнить после загрузке bootstrap.bundle.min.js, а bootstrap.bundle.min.js следует загрузить только после загрузки jquery.min.js
loadJs('https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js',
  () => {
    loadJs('https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js',
      () => {
        setTimeout(() => {
          $('.modal').modal('show');
        }, 1000);
      },
      () => {
        console.error('Ошибка при загрузки скриптов!');
      }
    )
  },
  () => {
    console.error('Ошибка при загрузки скриптов!');
  }
)

Решение

Комментарии: 2

ctac
ctac
мощно, спасибо! но все равно как -то сложно!
Александр Мальцев
Александр Мальцев
Пожалуйста!