Promises (Обещания)
Всем привет. На повестке сегодняшнего дня – Promises
.
У многих возникает много непонимания как работать с обещаниями, а у многие даже не понимают зачем они вообще нужны. В этой статье, я, как и всегда, постараюсь максимально просто и на примерах рассказать о обещаниях. Надеюсь, что после прочтения этой статьи ты больше никогда не будешь прятаться от Promises
, ведь они совсем не страшные =)
Начнем
Хотелось бы начать с того, что JS язык синхронный, т.е. весь код выполняется последовательно. И все было бы хорошо, но достаточно быстро в языке появилась потребность в дополнительных возможностях, а именно, понадобилась асинхронность. Зачем?
Все мы сидим в социальных сетях. Когда ты заходишь, например, в Facebook, то наверное, замечал, что сайт загружается за несколько секунд, а не "висит" по несколько минут. Но все бы сайты, тем более такие крупные как Facebook без асинхронных операций загружались бы очень долго.
Ведь когда ты заходишь на Facebook – браузер отсылает очень много запросов на сервера соц.сети, чтобы получить какие-то данные, например, фотографию твоего профиля, информацию из этого профиля, личные сообщения, списки друзей и т.п. На все это нужно время, и если бы все эти запросы выполнялись друг за другом, т.е. каждый бы запрос ожидал завершения предыдущего – это был бы ад.
Благодаря асинхронности, все запросы отсылаются одновременно и весь остальной код не ждет ответа от них, а продолжает выполняться. Когда же асинхронная операция заканчивает свое выполнение – отрабатывает какая-либо заранее подготовленная функция, которая, например, отобразит блок с твоими друзьями или все сообщения в определенном диалоге.
Какие асинхронные операции мы знаем?
Я уже писал о асинхронных методах в JS, а конкретно: setInterval
и setTimeout
.
Это яркие представители асинхронного исполнения кода. Если не читал о них, то советую прочитать (делай тык):
Эмуляция работы с сервером с помощью setTimeout
Итак, давай попробуем сэмулировать работу с сервером. Сначала, сделаем это с помощью setTimeout
, а затем с помощью Promise
и, как итог, поймем в чем разница и рассмотрим все плюсы Promise
.
Представим, что мы отсылаем запрос на сервер. Сервер собирает данные (на это уходит какое-то время) и сервер высылает нам данные (это тоже занимает время).
Пошли к коду:
console.log('Отправляем запрос на сервер...'); setTimeout(function() { console.log('Сервер собирает данные...'); const data = { text: 'Данные с сервера' }; setTimeout(function() { data.other = true; console.log('Данные, которые предоставил сервер: ', data); }, 2000); }, 1500);
Итак, весь процесс нашей эмуляции:
- Эмулируем отправку запроса
- Создаем первый
setTimeout
, который отработает через 1.5 секунды. Этот таймаут будет эмулировать "сбор данных" и создавать объектdata
с этими данными. - Внутри первого
setTimeout
создаем второй. Он будет как-то дополнять/изменять объектdata
и как бы высылать пользователю и потратит он на это 2 секунды.
Итог работы:
Все прикольно. Отработало это ровно так как и ожидали за 3.5 секунды.
Вроде бы все прикольно, да не совсем. Мне лично, даже смотреть на такой код больно. setTimeout
в setTimeout
-е. Ведь если мы сейчас захотим еще что-то эмулировать, то у нас будет еще одна вложенность и все это будет напоминать матрешку. Разбираться в таких "матрешках" – не самое приятное удовольствие.
Поэтому, давай попробуем все это же провернуть с помощью Promise
.
Эмуляция работы с сервером с помощью Promise
Сначала создадим пустой Promise
. Делается это так:
const promise = new Promise(function(resolve, reject) {});
Создается обещание с помощью класса Promise
, поэтому используется ключевое слово new
. В конструктор данного класса передается всего один аргумент –callback
-функция, которая, в свою очередь, принимает в себя 2 аргумента:
resolve
(переводится как "разрешить")reject
(переводится как "отклонить")
Эти аргументы, на самом деле являются функциями. Благодаря этим 2-ум функциям мы можем контролировать выполнение Promise
. К примеру, внутри обещания у нас будут какие-то проверки. Если все проверки будут выполнены удачно, то мы вызовем функцию resolve
и, в таком случае, Promise
завершится удачно. А если же, какая-то проверка не будет пройдена, то мы сможем завершить работу Promise
с помощью вызова функции reject
.
Пока что, скорее всего совсем ничего не понятно. Но, сейчас, мы разберемся со всем этим делом, не переживай.
Сейчас мы сделаем ту же самую эмуляцию работы с сервером с помощью Promise
Мы уже создали обещание, но оно еще никак не функционирует. Поэтому, давай переносить функционал из кода написанного ранее.
console.log('Отправляем запрос на сервер...'); const promise = new Promise(function(resolve, reject) { setTimeout(function() { const data = { text: 'Данные с сервера' }; }, 2000); });
Итак, внутри callback
-функции нашего Promise
, мы разместили код:
setTimeout(function() { const data = { text: 'Данные с сервера' }; }, 2000);
Это код нашего первого setTimeout
из кода выше. Давай добавим в этот setTimeout
вызов функции resolve()
, так как мы хотим чтобы наш Promise
выполнился без ошибок. В итоге получаем такой код:
const promise = new Promise(function(resolve, reject) { setTimeout(function() { console.log('Сервер собирает данные...'); const data = { text: 'Данные с сервера' }; resolve(); //успешное выполнение Promise }, 1500); });
Итак, как работать с этим всем дальше? У callback-функции Promise
, как я и написал ранее существует две функции: resolve
, reject
.
Вызвав функцию resolve
в коде выше, мы, как бы послали сигнал, что Promise
успешно выполнился. Но как отловить этот сигнал? На самом деле в этом нет ничего сложно.
В константу promise
мы записали наш Promise
, поэтому мы можем работать с ней следующим образом:
promise.then(function() { console.log('Успешное выполнение Promise'); });
Но, в целом, в этом случае (как и во многих других) лучше использовать стрелочную функцию в качестве callback:
promise.then(() => console.log('Успешное выполнение Promise'));
У Promise
существует метод then
, который ожидает, что в него ты передашь callback-функцию, которая выполнится только в тот момент, когда внутри самого Promise
мы вызовем функцию resolve
, которая означает успешное выполнение обещания.
Итог работы нашего кода:
Итак, первую часть мы реализовали с помощью Promise
. Пока что непонятно, чем же Promise
лучше и в чем их профит. Но давай продолжим перетаскивать код дальше.
На самом деле, третьим сообщением, в соответствии с первой реализацией эмуляции должен выводится текст: "Данные, которые предоставил сервер..." и дополнительно должен выводится сам объект data
, а не "Успешное выполнение Promise". Давай это поправим, поэтому вернемся к этой строке:
promise.then(() => console.log('Успешное выполнение Promise'));
Итак, здесь мы должны заменить текст и вывести объект data
. Но вот в чем проблема – здесь, в этой callback
-функции у нас нет никакого объекта data
, следственно вывести мы его не можем. Чтобы решить этот вопрос и получить доступ к объекту data
нужно всего лишь в метод resolve
, который мы вызываем в Promise
передать наш объект data
. Вернемся к нашему коду и поправим вызов resolve
:
const promise = new Promise(function(resolve, reject) { setTimeout(function() { console.log('Сервер собирает данные...'); const data = { text: 'Данные с сервера' }; resolve(data); //передаем data }, 1500); });
Теперь в функцию resolve
мы передаем наш объект data
. Что же это нам дает? А дает это нам возможность получить этот объект в методе then
. И вот как это делается.
Вот это:
promise.then(() => console.log('Успешное выполнение Promise'));
Меняем на:
promise.then(data => console.log('Данные, которые предоставил сервер: ', data) );
Как видишь, теперь у нас стрелочная функция имеет аргумент data
– и в этот аргумент и попадает то, что мы передаем внутрь функции resolve
при ее вызове.
Как итог, получаем:
И все бы хорошо, да вот только объект в итоге имеет совсем не тот итоговый вид, как в первом случае.
На данный момент мы перенесли только один setTimeout
, а у нас их было два.
Во-втором setTimeout
мы добавляли дополнительно поле other
со значением true
к нашему объекту data
.
Получается, что мы пропустили момент модификации объекта. Давай восполним данную потерю.
Нам нужно в какой-то момент модифицировать объект data
и добавить ему свойство other
. Когда же нам это сделать? Сделать нам это нужно тут:
promise.then(data => console.log('Данные, которые предоставил сервер: ', data) );
Вместо того, чтобы просто выполнить console.log
, нам нужно изменить объект data
, который приходит к нам из resolve
выполненного в Promise
. Более того, нам нужно выполнить это только через 2 секунды, а это значит, что нам нужно добавить наш setTimeout
.
Давай попробуем исправить наш код:
promise.then(data => setTimeout(function() { data.other = true; console.log('Данные, которые предоставил сервер: ', data); }, 2000) );
Итог:
И вот, вроде бы уже и можно вскрикнуть "Ура". Но, нет. На самом деле, мы схалтурили.
Первый setTimeout
, мы реализовали внутри Promise
, что обеспечило нам контроль над происходящим: можем вызвать resolve
для того, чтобы указать на успешное выполнение и reject
– на выполнение с ошибкой.
Сейчас же, наш второй setTimeout
не имеет такой возможности. А все потому, что мы не использовали Promise
. Давай используем его и поправим наш имеющийся код. Для того, чтобы добавить Promise
, нам нужно, чтобы callback
-функция, которую мы определяем внутри then
– возвращала нам новый Promise.
Поэтому давай снова изменим callback
-функцию в then
:
promise.then(data => new Promise(function(resolve, reject) {}));
Теперь мы сделали так, что then
вернет нам новый Promise
. Пока что он ничего не выполняет, поэтому давай добавим наш setTimeout
в тело callback
-функции нашего нового Promise
:
promise.then(data => new Promise(function(resolve, reject) { setTimeout(function() { data.other = true; //не забываем выполнять resolve(data) resolve(data); }, 2000) }));
Этим этапом мы изменили наш объект data
. И с помощью resolve(data)
мы сообщили, что наш новый (второй) Promise
выполнился успешно и передали наш объект data
дальше.
Но мы еще не выводим сообщение о том, что сервер предоставил нам какие-то данные. Давай поправим и это. Так как у нас из then
возвращается новый Promise
, то это означает, что мы можем использовать тот же метод then
к этому обещанию и как-то отреагировать на новый вызов resolve(data)
:
promise.then(data => new Promise(function(resolve, reject) { setTimeout(function() { data.other = true; //не забываем выполнять resolve(data) resolve(data); }, 2000) }) ).then(data => console.log('Данные, которые предоставил сервер: ', data) );
И вот теперь у нас все выполняется абсолютно так, как нужно.
Итак, весь наш код выглядит так:
По количеству строк, относительно изначальной реализации – кода прибавилось. Но если задуматься, то в первой нашей реализации мы никак не управляли состоянием выполнения и не могли на него повлиять.
С Promise
же, мы имеем контроль над выполнением нашего кода. Если код успешно выполнился, то мы выполняем функцию resolve
и обработчик then
сразу же отлавливает это и выполняет заданные нами действия. Это же круто? Безусловно.
Но, мы пока что не затронули метод reject
. Поэтому, давай поговорим и о нем.
Метод reject
Говорим и говорим о resolve
, а reject
как-будто, вообще никто и звать никак.
На самом деле метод reject
не менее полезный, но служит он для той цели, чтобы сообщить о том, что наш Promise
должен завершиться ошибкой.
Ты уже знаешь, что обработчиком выполнения функции resolve
служит метод then
.
У функции reject
, свой обработчик – catch
.
Если посмотреть еще раз на последний скриншот с кодом, то можно заметить, что Promise
создают цепочку вызовов методов then
:
const promise = new Promise(...); promise.then( ... код ... ).then( ... код ... )
Так вот, во всей этой цепочке, методу catch
самое место – практически в самом ее конце (почему практически – узнаешь дальше):
const promise = new Promise(...); promise.then( ... код ... ).then( ... код ... ).catch( ...обработка ошибки... )
Внутри этого catch
мы можем каким-то образом обработать ошибку и как и с функцией resolve
, мы можем передать эту самую ошибку в качестве аргумента функции reject(error)
.
Для примера, я поменяю в нашем коде один из resolve
на reject
и передам в качестве аргумента ошибку:
const promise = new Promise(function(resolve, reject) { setTimeout(function() { console.log('Сервер собирает данные...'); const data = { text: 'Данные с сервера' }; reject(new Error('Ошибка сбора данных')); //передаем ошибку }, 1500); }); promise.then(data => new Promise(function(resolve, reject) { setTimeout(function() { data.other = true; //не забываем выполнять resolve(data) resolve(data); }, 2000) }) ).then(data => console.log('Данные, которые предоставил сервер: ', data) ).catch(err => console.error(err));
Все что мы сделали – это вызвали reject
и навесили обработчик catch
. И теперь, если запустить наш код, мы получим ошибку:
Метод finally
Кроме методов then
и catch
, существует еще один метод – finally
.
Метод finally
выполняется всегда, вне зависимости от того вызвали мы внутри обещания resolve
или reject
.
Этот метод мы размещаем в самом конце цепочки и итоговый код у нас получается таким:
const promise = new Promise(function(resolve, reject) { setTimeout(function() { console.log('Сервер собирает данные...'); const data = { text: 'Данные с сервера' }; resolve(data); //генерируем ошибку //reject(new Error('Ошибка сбора данных')); }, 1500); }); promise.then(data => new Promise(function(resolve, reject) { setTimeout(function() { data.other = true; //не забываем выполнять resolve(data) resolve(data); }, 2000) }) ).then(data => console.log('Данные, которые предоставил сервер: ', data) ).catch(err => console.error(err) ).finally(() => console.log('Работа с сервером завершена') );
finally
действительно не важно, будет ошибка:
или ее не будет:
Он будет выполняться всегда.
Домашка
Итак, домашнее задание придумать не просто. Поэтому, я оставлю листинг кода в онлайн-редакторе, попробуй поиграться сам с кодом и разобраться, если какие-то моменты остались недопонятыми. Ну, и, конечно же, если совсем будет много вопросов – всегда можешь писать в наш чат (ссылка на него в группе, в закрепленном сообщении).