Промисы (Promises), или Обещания, в JavaScript появились в стандарте EcmaScript2015 (ES6) и с тех пор надежно обосновались в нем для обработки асинхронных операций.
Разработчики дали промисам такое название неслучайно. Promise переводится с английского, как обещание, и как любое обещание может быть либо выполнен, либо не выполнен – и третьего не дано. Для отслеживания этих двух результатов у него есть 2 соответствующие функции – resolve() и reject() и 3 состояния:
- ожидание (pending): начальное состояние, не исполнен и не отклонен.
- исполнено (fulfilled): операция завершена успешно.
- отклонено (rejected): операция завершена с ошибкой.
Т.е. при создании промис сначала находится в режиме ожидания, пока не выполнится некий записанный вами код, а затем переходит только в одно из возможных состояний: fulfilled – при успешном завершении вашего кода или rejected – при неуспешном его выполнении (например, ошибке соединения с сервером). После этого вы передаете управление кодом 2-м дополнительным функциям, которые каким-либо образом обрабатывают успех или неудачу вашего кода. При успешном результате вызывается функция then(), ошибка же обрабатывается в функции catch().

Если перевести then() с английского (затем, потом, тогда), то можно сказать, что данный метод что-то делает после того, как отработает функция resolve(), т.е. он потом отслеживает успешное выполнение некоего задания. Метод catch() переводится, как ловить или поймать, и нужен, чтоб отловить ошибку из функции reject(), аналогично тому, как это выполняется в конструкции try…catch().
Давайте посмотрим на поддержку браузерами объекта Promise. Сайт caniuse.com выдает нам такую информацию:
Т.е. за исключением Internet Explorer и Opera Mini, которые традиционно мало что из нововведений поддерживают, мы можем использовать промисы в большинстве современных браузеров.
Разберемся с этой технологией подробнее.
Пример со случайным числом на основе промисов
Рассмотрим пример, в котором будем имитировать игру “Случайный номер”. С помощью математической функции Math.random() будем получать случайный номер в диапазоне от 0 до 400, но демонстрировать пользователю результат будем с некоторой задержкой, имитируя либо соединение с сервером, либо “задумчивость” генератора случайных чисел.
Мы используем функции resolve() и reject(), предоставляемые промисами по умолчанию, для того чтобы отследить, в какой диапазон попало наше случайное число, и в зависимости от этого выведем сообщение с помощью функции then() и catch().
Код примера таков:
<script>
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’, ‘<li style=”color: red”>’ + mes + ‘</li>’))
.catch(mes => output.insertAdjacentHTML(‘beforeend’, ‘<li style=”color: gray”>’ + mes + ‘</li>’))
}
if (“Promise” in window) {
let btn = document.getElementById(“btn”);
btn.addEventListener(“click”, testPromise);
} else {
output.innerHTML = ‘К сожалению, ваш браузер не поддерживает интерфейс <code style=”font-size: 18px”>Promise<code>.’;
}
</script>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<div class=“test”>
<button id=“btn” class=“btn btn-primary”>Создать Promise. Играть в числа</button>
<ol id=“output”></ol>
</div>
<script>
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’, ‘<li style=”color: red”>’ + mes + ‘</li>’))
.catch(mes => output.insertAdjacentHTML(‘beforeend’, ‘<li style=”color: gray”>’ + mes + ‘</li>’))
}
if (“Promise” in window) {
let btn = document.getElementById(“btn”);
btn.addEventListener(“click”, testPromise);
} else {
output.innerHTML = ‘К сожалению, ваш браузер не поддерживает интерфейс <code style=”font-size: 18px”>Promise<code>.’;
}
</script>
|
Протестируйте пример сами, нажав на кнопку несколько раз.
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’, ‘
- ‘ + mes + ”))
.catch(mes => output.insertAdjacentHTML(‘beforeend’, ‘ - ‘ + mes + ”))
}
if (“Promise” in window) {
let btn = document.getElementById(“btn”);
btn.addEventListener(“click”, testPromise);
} else {
output.innerHTML = ‘К сожалению, ваш браузер не поддерживает интерфейсPromise.';
}Этот пример в IE выдаст нам то предупреждение, которое мы установили при проверке:

Пример с подсчетом чисел с помощью Promise
Если в предыдущем примере от значения случайного числа зависело то, выиграем мы или проиграем, то в примере ниже то же случайное число мы используем для проверки того, как хорошо или плохо пользователь считает сумму чисел.
123456789101112131415161718192021222324<div class="test"><p id="testUserNum"></p><button class="button" onclick="doTestUser()">Давайте свой тест!</button></div><script>function doTestUser(){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(`Вы совершенно правы! <strong>${num1} + ${num2} = ${num1+num2}</strong> `);else reject({message: `Ваш ответ <strong>${num1} + ${num2} = <span style="color:red">${userNum}</span></strong> не совпадает с верным результатом <strong style="color: #0791bb">${num1+num2}</strong>`, 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;})}</script>Теперь вызов функции
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()при вызове любой функции. Например, мы последовательно выводим такой код:1234console.log('Start 1 👍');console.log('Start 2👋🏻');console.log('Start 3 🧍🏼♂️');console.log('Start 4 🏃🏼');И именно в таком порядке он появляется в консоли.

Однако, стоит добавить
setTimeout()с разными значениями задержек - и последовательность вывода меняется в зависимости от значения этих задержек. Этот метод позволяет показать, что действия в JavaScript могут выполнятся асинхронно и непоследовательно.1234console.log('Start 1 👍');setTimeout( function() {console.log('Start 2👋🏻')}, 2000);setTimeout( function() {console.log('Start 3 🧍🏼♂️')}, 1000);console.log('Start 4 🏃🏼');
На скриншоте хорошо видно,что строчки 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 в случае недоступности сервера и т.д. (почитать подробнее).
В примере ниже мы будем случайным образом генерировать пустую строку или слово 'no', которое при добавлении к URL-адресу запроса будет нам давать ошибку 404 (файл не найден). Мы же эту ошибку будем обрабатывать с помощью ключевого слова throwи выводить в диалоговом окнеalert()внутри блокаcatch().12let rand = Math.round(Math.random()) ? '' : 'no';fetch(`https://jsonplaceholder.typicode.com/todos${rand}?_start=3&_limit=10`)Кстати, обратите внимание, что в этом запросе мы используем данные в виде параметра
_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() поэтому, пишем
1234const response = await fetch('https://jsonplaceholder.typicode.com/posts?userId=1');console.log(response);const posts = await response.json();console.log(posts);Такой синтаксис даст нам возможность записать в переменную response ответ от сервера а в переменную posts - результат работы метода
json()в виде массива объектов:
Пример для тестирования: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будет также отклонен.
1234let 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 => document.write(`<p>${values.join(' | ')}</p>`))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.


(Пока оценок нет)