Введение в промисы в JavaScript

Вы уже знаете, что использование коллбэк-модели асинхронности легко приводит к ситуации 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); });