Promises (Обещания)
JavaScript простым языком
Last updated
Was this helpful?
JavaScript простым языком
Last updated
Was this helpful?
Хотелось бы начать с того, что JavaScript
язык синхронный, т.е. весь код выполняется последовательно. И всё бы хорошо, но достаточно быстро в языке появилась потребность в дополнительных возможностях, а именно, понадобилась асинхронность. Для чего?
Все мы сидим в социальных сетях. Когда ты заходишь, например, в Facebook, то наверное, замечал, что сайт загружается за несколько секунд, а не висит по несколько минут. Все крупные сайты, как Facebook не смогли бы так быстро грузиться без асинхронных операций.
Ведь когда ты заходишь на Facebook – браузер отсылает очень много запросов на сервера соц.сети, чтобы получить какие-то данные. Например, фотографию твоего профиля, информацию из этого профиля, личные сообщения, списки друзей и т.п. На всё это нужно время, и если бы все эти запросы выполнялись друг за другом, т.е. каждый бы запрос ожидал завершения предыдущего – это было бы долго.
Благодаря асинхронности, все запросы отсылаются одновременно и весь остальной код не ждёт ответа от них, а продолжает выполняться. Когда же асинхронная операция заканчивает своё выполнение – отрабатывает какая-либо заранее подготовленная функция, которая, например, отобразит блок с твоими друзьями или все сообщения в определенном диалоге.
Я уже писал об асинхронных методах в JavaScript
, а конкретно: setInterval
и setTimeout
.
Это яркие представители асинхронного исполнения кода. Если не читал о них, то советую прочитать (делай тык):
Итак, давай попробуем сэмулировать работу с сервером. Сначала, сделаем это с помощью setTimeout
, а затем с помощью Promise
и, как итог, поймем в чем разница и рассмотрим все плюсы Promise
.
Представим, что мы отсылаем запрос на сервер. Сервер собирает данные (на это уходит какое-то n
время) , потом сервер высылает нам данные (это тоже занимает время).
Пошли к коду:
Итак, весь процесс нашей эмуляции:
Эмулируем отправку запроса
Создаём первый setTimeout
, который отработает через 1.5
секунды. Этот таймаут будет эмулировать сбор данных и создаст объект data
с этими данными.
Внутри первого setTimeout
создаем второй. Он будет как-то дополнять/изменять объект data
и как бы высылать пользователю и потратит он на это 2
секунды.
Итог работы:
Все прикольно. Отработало это ровно так как и ожидали за 3.5
секунды.
Вроде бы все прикольно, да не совсем. Мне лично, даже смотреть на такой код больно. setTimeout
в setTimeout
-е. Ведь если мы сейчас захотим ещё что-то эмулировать, то у нас будет ещё одна вложенность и все это будет напоминать матрешку. Разбираться в таких матрешках – не самое приятное удовольствие.
Поэтому, давай попробуем всё то же самое провернуть с помощью Promise
.
Сначала создадим пустой Promise
. Делается это так:
Создается обещание с помощью класса Promise
, поэтому используется ключевое слово new
. В конструктор данного класса передаётся всего один аргумент – callback-функция, которая, в свою очередь, принимает в себя 2 аргумента:
resolve
(переводится как разрешить)
reject
(переводится как отклонить)
На самом деле эти аргументы являются функциями. Благодаря этим 2-м функциям мы можем контролировать выполнение Promise
. К примеру, внутри обещания у нас будут какие-то проверки. Если все проверки будут выполнены удачно, то мы вызовем функцию resolve
и, в таком случае, Promise
завершится удачно. А если же, какая-то проверка не будет пройдена, то мы сможем завершить работу Promise
с помощью вызова функции reject
.
Скорее всего ты пока ничего не понял. Не страшно, сейчас разберёмся со всем этим делом, не переживай.
Мы сделаем ту же самую эмуляцию работы с сервером с помощью Promise
Мы уже создали обещание, но оно ещё никак не функционирует. Поэтому, давай переносить функционал из кода написанного ранее.
Итак, внутри callback-функции нашего Promise
, мы разместили код:
Это код нашего первого setTimeout
из кода выше. Давай добавим в этот setTimeout
вызов функции resolve()
, так как мы хотим чтобы наш Promise
выполнился без ошибок. В итоге получаем такой код:
Итак, как работать с этим всем дальше? У callback-функции Promise
, как я и написал ранее существует две функции: resolve
, reject
.
Вызвав функцию resolve
в коде выше, мы, как бы послали сигнал, что Promise
успешно выполнился. Но как отловить этот сигнал? На самом деле в этом нет ничего сложного.
В константу promise
мы записали наш Promise
, поэтому мы можем работать с ней следующим образом:
Но, в целом, в этом случае (как и во многих других) лучше использовать стрелочную функцию в качестве callback
:
У Promise
существует метод then
, который ожидает, что в него ты передашь callback-функцию, она выполнится только в тот момент, когда внутри самого Promise
мы вызовем функцию resolve
, означающая успешное выполнение promise
.
Итог работы нашего кода:
Итак, первую часть мы реализовали с помощью Promise
. Пока что непонятно, чем же Promise
лучше и в чём их профит. Но давай продолжим перетаскивать код дальше.
На самом деле, третьим сообщением, в соответствии с первой реализацией эмуляции должен выводится текст: «Данные, которые предоставил сервер…» и дополнительно должен выводится сам объект data
, а не «Успешное выполнение Promise». Давай это поправим, поэтому вернемся к этой строке:
Итак, здесь мы должны заменить текст и вывести объект data
. Но вот в чём проблема – здесь, в этой callback-функции у нас нет никакого объекта data
, следовательно, вывести мы его не можем. Чтобы решить данный вопрос и получить доступ к объекту data
нужно всего лишь в метод resolve
, который мы вызываем в Promise
передать наш объект data
. Вернёмся к нашему коду и поправим вызов resolve
:
Теперь в функцию resolve
мы передаём наш объект data
. Что же это нам даёт? А даёт это нам возможность получить этот объект в методе then
. И вот как это делается.
Вот это:
Меняем на:
Как видишь, теперь у нас стрелочная функция имеет аргумент data
– и в этот аргумент и попадает то, что мы передаём внутрь функции resolve
при её вызове.
Как итог, получаем:
И все бы хорошо, да вот только объект в итоге имеет совсем не тот итоговый вид, как в первом случае.
На данный момент мы перенесли только один setTimeout
, а у нас их было два.
Во втором setTimeout
мы добавляли дополнительно поле other
со значением true
к нашему объекту data
.
Получается, что мы пропустили момент модификации объекта. Давай восполним данную потерю.
Нам нужно в какой-то момент модифицировать объект data
и добавить ему свойство other
. Когда же нам это сделать? Сделать нам это нужно здесь:
Вместо того, чтобы просто выполнить console.log
, нам нужно изменить объект data
, который приходит к нам из resolve
выполненного в Promise
. Более того, нам нужно выполнить это только через 2
секунды, а это значит, что нам нужно добавить наш setTimeout
.
Давай попробуем исправить наш код:
Итог:
И вот, вроде бы уже можно вскрикнуть «Ура». Fail. На самом деле, мы схалтурили.
Первый setTimeout
, мы реализовали внутри Promise
, что обеспечило нам контроль над происходящим: можем вызвать resolve
для того, чтобы указать на успешное выполнение и reject
– на выполнение с ошибкой.
Сейчас же, наш второй setTimeout
не имеет такой возможности. А всё потому, что мы не использовали Promise
. Давай используем его и поправим наш имеющийся код. Для того, чтобы добавить Promise
, нам нужно, чтобы callback-функция, которую мы определяем внутри then
– возвращала нам новый Promise.
Поэтому давай снова изменим callback-функцию в then
:
Теперь мы сделали так, что then
вернёт нам новый Promise
. Пока что он ничего не выполняет, поэтому давай добавим наш setTimeout
в тело callback-функции нашего нового Promise
:
Этим этапом мы изменили наш объект data
. И с помощью resolve (data)
мы сообщили, что наш новый (второй) Promise
выполнился успешно и передал наш объект data
дальше.
Но мы ещё не выводим сообщение о том, что сервер предоставил нам какие-то данные. Давай поправим и это. Так как у нас из then
возвращается новый Promise
, то это означает, что мы можем использовать тот же метод then
к этому обещанию и как-то отреагировать на новый вызов resolve (data)
:
И вот теперь у нас все выполняется абсолютно так, как нужно.
Итак, весь наш код выглядит следующим образом:
По количеству строк, относительно изначальной реализации – кода прибавилось. Но если задуматься, то в первой нашей реализации мы никак не управляли состоянием выполнения и не могли на него повлиять.
С Promise
же, мы имеем контроль над выполнением нашего кода. Если код успешно выполнился, то мы выполняем функцию resolve
и обработчик then
сразу же отлавливает это и выполняет заданные нами действия. Это же круто? Безусловно.
Но, мы пока что не затронули метод reject
. Поэтому, давай поговорим и о нём.
Говорим и говорим о resolve
, а reject
как будто, вообще никто и звать никак.
На самом деле метод reject
не менее полезный, но служит он для той цели, чтобы сообщить о том, что наш Promise
должен завершиться ошибкой.
Ты уже знаешь, что обработчиком выполнения функции resolve
служит метод then
.
У функции reject
, свой обработчик – catch
.
Если посмотреть ещё раз на последний пример с кодом, то можно заметить, что Promise
создаёт цепочку вызовов методов then
:
Так вот, во всей этой цепочке, методу catch
самое место – практически в самом её конце (почему практически – узнаешь дальше):
Внутри этого catch
мы можем каким-то образом обработать ошибку и, как и с функцией resolve
, мы можем передать эту самую ошибку в качестве аргумента функции reject(error)
.
Для примера, я поменяю в нашем коде один из resolve
на reject
и передам в качестве аргумента ошибку:
Все что мы сделали – это вызвали reject
и навесили обработчик catch
. И теперь, если запустить наш код, мы получим ошибку:
Кроме методов then
и catch
, существует еще один метод – finally
.
Метод finally
выполняется всегда, вне зависимости от того вызвали мы внутри обещания resolve
или reject
.
Этот метод мы размещаем в самом конце цепочки и итоговый код у нас получается таким:
finally
действительно не важно, будет ошибка:
или ее не будет:
Он будет выполняться всегда.
Итак, домашнее задание придумать не просто. Поэтому, я оставлю листинг кода в онлайн-редакторе, попробуй самостоятельно поиграться с кодом и разобраться. Ну, и, конечно же, если совсем будет много вопросов – всегда можешь писать в наш чат ( на него в , в закрепленном сообщении).