Promises (Обещания)

JavaScript простым языком

Хотелось бы начать с того, что JavaScript язык синхронный, т.е. весь код выполняется последовательно. И всё бы хорошо, но достаточно быстро в языке появилась потребность в дополнительных возможностях, а именно, понадобилась асинхронность. Для чего?

Все мы сидим в социальных сетях. Когда ты заходишь, например, в Facebook, то наверное, замечал, что сайт загружается за несколько секунд, а не висит по несколько минут. Все крупные сайты, как Facebook не смогли бы так быстро грузиться без асинхронных операций.

Ведь когда ты заходишь на Facebook – браузер отсылает очень много запросов на сервера соц.сети, чтобы получить какие-то данные. Например, фотографию твоего профиля, информацию из этого профиля, личные сообщения, списки друзей и т.п. На всё это нужно время, и если бы все эти запросы выполнялись друг за другом, т.е. каждый бы запрос ожидал завершения предыдущего – это было бы долго.

Благодаря асинхронности, все запросы отсылаются одновременно и весь остальной код не ждёт ответа от них, а продолжает выполняться. Когда же асинхронная операция заканчивает своё выполнение – отрабатывает какая-либо заранее подготовленная функция, которая, например, отобразит блок с твоими друзьями или все сообщения в определенном диалоге.

Какие асинхронные операции мы знаем?

Я уже писал об асинхронных методах в JavaScript, а конкретно: setInterval и setTimeout.

Это яркие представители асинхронного исполнения кода. Если не читал о них, то советую прочитать (делай тык):

Эмуляция работы с сервером с помощью setTimeout

Итак, давай попробуем сэмулировать работу с сервером. Сначала, сделаем это с помощью setTimeout, а затем с помощью Promise и, как итог, поймем в чем разница и рассмотрим все плюсы Promise.

Представим, что мы отсылаем запрос на сервер. Сервер собирает данные (на это уходит какое-то n время) , потом сервер высылает нам данные (это тоже занимает время).

Пошли к коду:

console.log('Отправляем запрос на сервер...');

setTimeout(function() {
   console.log('Сервер собирает данные...');

   const data = {
     text: 'Данные с сервера'
   };

  setTimeout(function() {
    data.other = true;
    console.log('Данные, которые предоставил сервер: ', data);
  }, 2000);
}, 1500);

Итак, весь процесс нашей эмуляции:

  1. Эмулируем отправку запроса

  2. Создаём первый setTimeout, который отработает через 1.5 секунды. Этот таймаут будет эмулировать сбор данных и создаст объект data с этими данными.

  3. Внутри первого 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. Пока что непонятно, чем же 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)
);

Итог:

И вот, вроде бы уже можно вскрикнуть «Ура». Fail. На самом деле, мы схалтурили.

Первый 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 действительно не важно, будет ошибка:

или ее не будет:

Он будет выполняться всегда.

Домашнее задание

Итак, домашнее задание придумать не просто. Поэтому, я оставлю листинг кода в онлайн-редакторе, попробуй самостоятельно поиграться с кодом и разобраться. Ну, и, конечно же, если совсем будет много вопросов – всегда можешь писать в наш чат (ссылка на него в группе, в закрепленном сообщении).

Ссылка на код

Last updated