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 действительно не важно, будет ошибка:
или ее не будет:
Он будет выполняться всегда.
Домашка
Итак, домашнее задание придумать не просто. Поэтому, я оставлю листинг кода в онлайн-редакторе, попробуй поиграться сам с кодом и разобраться, если какие-то моменты остались недопонятыми. Ну, и, конечно же, если совсем будет много вопросов – всегда можешь писать в наш чат (ссылка на него в группе, в закрепленном сообщении).