Вы уже знаете, что использование коллбэк-модели асинхронности легко приводит к ситуации callback hell. Поэтому в JavaScript была введена новая модель под названием промисы (англ. promise, обещание). Давайте изучим эту модель.
Промис представляет собой объект, в который параметром передается функция, внутри которой нужно размещать наш асинхронный код:
let promise = new Promise(function() {
// асинхронный код
});
Как вы видите, я записал объект с промисом
в переменную promise
. В каком-то другом
месте кода я могу применить к этой переменной
метод then
, передав в него функцию
с кодом, который должен быть выполнен по
завершении асинхронного кода, написанного
при создании данного промиса:
promise.then(function() {
// выполнится при завершении асинхронного кода
});
Звучит запутано, поэтому давайте посмотрим на примере. Пусть у меня есть вот такой асинхронный код:
setTimeout(function() {
let result = [1, 2, 3, 4, 5];
}, 3000);
Пусть я хочу решить для него нашу основную
задачу асинхронности: выполнить некоторый
код после срабатывания таймера. При этом
я не хочу размещать этот код в самом таймере
и хочу, чтобы в этот код как-то попал результат,
написанный мною в переменной result
.
В общем-то, мы решали эту задачу в предыдущих
уроках через коллбэки и подписки. Давайте
теперь посмотрим, как это сделать через промисы.
Для начала нужно обернуть наш асинхронный код в промис:
let promise = new Promise(function() {
setTimeout(function() {
let result = [1, 2, 3, 4, 5];
}, 3000);
});
Этого, однако, не достаточно. Мы должны в явном виде указать, что наш асинхронный код завершился. В этом нам поможет специальная функция завершения, автоматически попадающая в первый параметр функции, если он указан:
let promise = new Promise(function(resolve) { // указываем параметр
setTimeout(function() {
let result = [1, 2, 3, 4, 5];
}, 3000);
});
С помощью функции завершения мы можем явно указать промису, что асинхронный код завершился. Для этого мы должны вызвать эту функцию в нужном нам месте:
let promise = new Promise(function(resolve) {
setTimeout(function() {
let result = [1, 2, 3, 4, 5];
resolve(); // завершаем промис
}, 3000);
});
При этом, если мы хотим передать вовне какой-то результат асинхронного кода, мы можем передать его параметром нашей функции завершения:
let promise = new Promise(function(resolve) {
setTimeout(function() {
let result = [1, 2, 3, 4, 5];
resolve(result); // передаем результат
}, 3000);
});
Можно, конечно же, избавится от промежуточной переменной:
let promise = new Promise(function(resolve) {
setTimeout(function() {
resolve([1, 2, 3, 4, 5]);
}, 3000);
});
Теперь в любом другом месте мы можем вызвать
метод then
нашего промиса:
promise.then(function() {
// сработает по завершению промиса
});
Результат работы промиса попадет в первый параметр функции, если мы пожелаем его указать:
promise.then(function(result) {
console.log(result); // выведет массив с результатом
});
Сделайте промис, внутри которого будет задержка
в 5
секунд, после которой промис должен
выполнится, своим результатом вернув какой-нибудь
текст. Выведите этот текст на экран.
Исключительные ситуации
Давайте теперь научимся обрабатывать исключительные ситуации, случившиеся внутри промиса. В случае возникновения такой ситуации мы должны отклонить промис с помощью специальной функции отклонения, которая автоматически попадает во второй параметр функции промиса:
let promise = new Promise(function(resolve, reject) {
setTimeout(function() {
...
}, 3000);
});
Внутри функции промиса мы должны вызвать
resolve
, если все прошло штатно, либо
reject
, если возникла исключительная
ситуация:
let promise = new Promise(function(resolve, reject) {
setTimeout(function() {
let isError = false;
if (!isError) {
resolve([1, 2, 3, 4, 5]); // данные промиса
} else {
reject('error in promise'); // ваш текст ошибки
}
}, 3000);
});
Затем в методе then параметрами вы должны передать не одну, а две функции: первая сработает, если промис сработал штатно (зарезолвился), а вторая - если сработал с ошибкой (зареджектился):
promise.then(
function(result) {
console.log(result); // выведет результат промиса
},
function(error) {
console.log(error); // выведет текст ошибки
}
);
Как правило, приведенный выше код записывают более компактно, вот так:
promise.then(function(result) {
console.log(result); // выведет результат промиса
}, function(error) {
console.log(error); // выведет текст ошибки
});
Сделайте асинхронный код, который будет генерировать
случайные числа от 0
до 5
.
Оберните все это в промис. Пусть промис своим
возвращает результат деления единицы на сгенерированное
число. Пусть промис выполнится с ошибкой,
если произошло деление на ноль, и с успехом
во всех остальных случаях.
Использование объекта с ошибкой
Более принято в функцию reject
передавать
не строку с ошибкой, а объект с ошибкой:
let promise = new Promise(function(resolve, reject) {
setTimeout(function() {
reject(new Error('error in promise')); // объект с ошибкой
}, 3000);
});
Можно также выбрасывать объекты с ошибками
с помощью throw
- это будет эквивалентно
передаче их в reject
:
let promise = new Promise(function(resolve, reject) {
setTimeout(function() {
throw new Error('error in promise'); // эквивалентно reject
}, 3000);
});
Только обработка ошибок
В then
можно указать только функцию-обработчик
исключительной ситуации, передав вместо первого
параметра null
:
promise.then(
null,
function(error) {
console.log(error);
}
);
В таком случае удобно воспользоваться сокращенным
синтаксисом через метод catch
:
promise.catch(
function(error) {
console.log(error);
}
);
Метод catch
полностью эквивалентен
методу then
, у которого первый параметр
null
.
Состояния промиса
Промис может находится в одном из трех состояний. При создании промис находится в ожидании (pending), а затем может стать исполненным (fulfilled), вернув полученный результат, или отклоненным (rejected), вернув причину отказа. Вы можете посмотреть переход из одного состояния в другой, запустив следующий код:
let promise = new Promise(function(resolve, reject) {
setTimeout(function() {
let isError = false; // поставьте либо true, либо false
if (!isError) {
resolve([1, 2, 3, 4, 5]);
} else {
reject('error in promise');
}
}, 3000);
});
setInterval(function() {
console.log(promise); // каждую секунду выводим промис в консоль
}, 1000);
Учтите, что состояния fulfilled и rejected неизменны: если промис перешел в одно из этих состояний, то он уже не сможет перейти в другое. Давайте посмотрим на примере. В следующем коде вызов reject случится раньше, поэтому вызов resolve будет проигнорирован:
let promise = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('error')), 1000);
setTimeout(() => resolve('ignored'), 2000);
});