May 22, 2020

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);

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

  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 лучше и в чем их профит. Но давай продолжим перетаскивать код дальше.

На самом деле, третьим сообщением, в соответствии с первой реализацией эмуляции должен выводится текст: "Данные, которые предоставил сервер..." и дополнительно должен выводится сам объект 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 действительно не важно, будет ошибка:

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

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

Домашка

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

Ссылка на код