Промисы (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()
.
Код примера таков:
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’, ‘
.catch(mes => output.insertAdjacentHTML(‘beforeend’, ‘
}
if (“Promise” in window) {
let btn = document.getElementById(“btn”);
btn.addEventListener(“click”, testPromise);
} else {
output.innerHTML = ‘К сожалению, ваш браузер не поддерживает интерфейс
Promise.';
}
Этот пример в IE выдаст нам то предупреждение, которое мы установили при проверке:
Пример с подсчетом чисел с помощью Promise
Если в предыдущем примере от значения случайного числа зависело то, выиграем мы или проиграем, то в примере ниже то же случайное число мы используем для проверки того, как хорошо или плохо пользователь считает сумму чисел.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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()
при вызове любой функции. Например, мы последовательно выводим такой код:
1
2
3
4
console.log('Start 1 👍');
console.log('Start 2👋🏻');
console.log('Start 3 🧍🏼♂️');
console.log('Start 4 🏃🏼');
И именно в таком порядке он появляется в консоли.
Однако, стоит добавить setTimeout()
с разными значениями задержек - и последовательность вывода меняется в зависимости от значения этих задержек. Этот метод позволяет показать, что действия в JavaScript могут выполнятся асинхронно и непоследовательно.
1
2
3
4
console.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()
.
1
2
let 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() поэтому, пишем
1
2
3
4
const 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
будет также отклонен.
1
2
3
4
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 => 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
.