Статьи по JavaScript

Promises (обещания) в JavaScript

Promises (далее “Обещания”) на Javascript, обозначили периоды “до” и “после” в истории веб-разработки.

В данный момент вы должны находиться в одной из следующих ситуаций:

  • Вы читали про “Обещания” в разных местах. Вы знаете, что это важная концепция в экосистеме Javascript, но вы не уверены, что такое Promise. Если это ваша ситуация, рекомендуем прочитать всю статью.
  • Вы уже использовали Javascript Promises. Однако вы так и не смогли до конца понять важность и полезность этой концепции. Если это ваш случай, вы можете начать с изучения терминологии.
  • Вы используете их в своей повседневной жизни. В этом случае не помешает освежить информацию.

Почему обещания важны в JS?

JavaScript является однопоточным. Это означает, что он может выполнять только одно действие за один раз, из единственного доступного потока.

Если у нас есть последовательность операций, то эти операции должны выполняться одна за другой (поскольку невозможно создать больше потоков).

Реализация JavaScript отличается в каждом браузере. Но обычно выполнение кода JavaScript происходит параллельно с процессом отрисовки элементов, обновления стилей и обработки действий пользователя (таких как выделение текста или взаимодействие с элементами формы). Активность в одном из них замедляет другие.

Например:

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

Однако существуют блокирующие операции, с которыми нам приходится иметь дело. Например, чихание.

Во время чихания приходится приостанавливать другие виды деятельности.

Это довольно раздражает, особенно когда вы действительно сосредоточены на выполнении нескольких действий одновременно.

В JS решением этого ограничения являются events и callbacks. Вы наверняка ими пользовались.

Вот пример событий:

const myImage = document.querySelector('#example'); myImage.addEventListener('load', function() { // Изображение загрузилось }); myImage.addEventListener('error', function() { // Возникли проблемы });
Code language: JavaScript (javascript)

Пока все хорошо.

Мы получаем ссылку на изображение, добавляем пару listeners, и наш код JavaScript будет повторно выполняться при наступлении одного из этих событий.

Однако в приведенном выше примере возможно, что события произошли до того, как мы начали их прослушивать. Вот почему важно проверить это (в данном случае путем оценки complete):

var myImage = document.querySelector('#example'); function loaded() { // Изображение загрузилось } if (myImage.complete) { loaded(); } else { myImage.addEventListener('load', loaded); } myImage.addEventListener('error', function() { // Возникли проблемы });
Code language: JavaScript (javascript)

Здесь мы не фиксируем ошибку, если она произошла до регистрации события (к сожалению, DOM не позволяет нам этого сделать).

Но главное здесь то, какие соображения мы должны учитывать при загрузке.

Как будет выглядеть код, если нас интересует выполнение действий после загрузки набора изображений?

События не всегда являются лучшим вариантом

События отлично подходят для обнаружения действий, которые повторяются несколько раз на одном и том же объекте (например, keyup, touchstart и т.д.).

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

Но когда мы имеем дело с асинхронными действиями, которые могут либо успешно завершиться, либо закончиться неудачей, в идеале нам нужно что-то вроде следующего:

myImage.doIfLoaded(function() { // Изображение загрузилось }).doIfFail(function() { // Загрузка не удалась }); // и так же... doWhenAllGood([myImage1, myImage2]).ejecutarEsto(function() { // Все изображения загружены }).doIfHappensFail(function() { // Не удалось загрузить изображение });
Code language: JavaScript (javascript)

Это именно то, что делают обещания .

Если бы у элементов HTML img был метод “ready”, возвращающий Promise, мы могли бы сделать следующее:

myImage.ready().then(function() { // загружено }, function() { // неудача }); // и так же ... Promise.all([myImage1.ready(), myImage2.ready()]).then(function() { // все загружены }, function() { // произошла ошибка });
Code language: JavaScript (javascript)

В принципе, обещания похожи на события со следующими отличиями:

  • Обещание может быть успешным или неудачным только один раз. Оно не может быть успешеным или неудачным во второй раз, а также не может сменить успех на неудачу позже, или наоборот.
  • Если обещание удалось или не удалось, и мы позже (просто) регистрируем callback success или failure, будет вызвана соответствующая callback-функция (даже если событие произошло раньше).

Это очень полезно для асинхронных операций, поскольку помимо фиксации точного момента, когда что-то происходит, мы сосредотачиваемся на реакции на произошедшее.

Терминология, связанная с Обещаниями

У нас есть много терминов, связанных с тем, что такое обещания в Javascript. Давайте рассмотрим основные понятия.

Обещание может иметь следующие состояния:

  • fulfilled – Действие, связанное с обещанием, было успешно завершено.
  • rejected – действие по обещанию не выполнено
  • pending – Еще не установлено, было ли обещание fulfilled или rejected
  • settled – Уже определено, было ли обещание fulfilled или rejected.

Также часто используется термин thenable, указывающий на то, что объект имеет доступный метод “then” (и поэтому связан с Promises).

Цепочка обещаний

Часто нам нужно выполнить 2 или более асинхронных операций друг за другом. То есть, следующая операция начинается после того, как предыдущая была успешно выполнена (либо потому, что предыдущее действие было необходимо для подготовки чего-либо, либо потому, что оно возвращает результат, который будет использован в последующих).

Это легко решается с помощью последовательности цепочек Promises.

Но давайте сначала посмотрим, как мы привыкли работать с непрерывными асинхронными операциями. Это привело к так называемому “callback hell”.

Предположим, нам нужно выполнить 3 действия. Наш код, использующий callbacks, будет выглядеть следующим образом:

doSomething(function(result) { doSecondThing(result, function(secondResult) { doAThirdThing(secondResult, function(finalResult) { console.log('Итоговый результат: ' + finalResult); }, failureCallback); }, failureCallback); }, failureCallback);
Code language: JavaScript (javascript)

Это выглядит ужасно. Но это то, что традиционно используется.

Если вы уже давно используете Javascript, я уверен, что вы хорошо его понимаете.

Но если вы этого не сделаете, вот что произойдет:

  • Функция “doSomething” регистрирует обратный вызов. Эта функция будет вызвана позже (после выполнения какого-либо действия) и получит параметр “result”.
  • Это полученное значение используется в качестве аргумента при вызове функции “doSecondThing”, которая в итоге определяет вторую функцию обратного вызова (которая ожидает “secondResult”).
  • Этот новый результат необходим для запуска третьей операции. Функция обратного вызова, определяющая “doAThirdThing”, вызывается последней (если все идет хорошо) и печатает конечный результат в консоль.
  • Если какая-либо функция из трех, которые были вызваны, терпит неудачу, то вызывается соответствующая функция failCallback. То есть, каждая функция, которую мы вызываем, имеет соответствующие обратные вызовы успеха и неудачи. Разница в том, что обратные вызовы успеха определены в примере, а обратные вызовы отказа считаются определенными (но мы указываем только их название).

Давайте рассмотрим более конкретный пример.

У нас есть веб-приложение, которое запрашивает API для получения информации. Каждый вызов API включает в себя тайм-аут.

Чтобы это не повлияло на все наше приложение, важно, чтобы эти операции выполнялись асинхронно.

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

Но сначала мы хотим знать, подтвердил ли пользователь свою электронную почту, потому что если нет, мы сначала предложим ему сделать это.

Поэтому код, который мы будем использовать, будет выглядеть следующим образом:

checkEmailVerification(function(emailComfirmed) { if (emailComfirmed) { getUserCats(userId, function(userCats) { if (userCats.length > 0) getPopularArticlesUser(userCats, function(getArticlesList) { console.log('Статьи в предпочтительных категориях: ' + getArticlesList); }, failureCallback); else getPupularAricles(function(getArticlesList) { console.log('Популярные статьи: ' + getArticlesList); }, failureCallback); }, failureCallback); } else { console.log('Сначала, пожалуйста, подтвердите свой адрес электронной почты.'); } }, failureCallback);
Code language: JavaScript (javascript)

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

Чтобы предотвратить подобное, мы можем использовать Promises.

Аналог, использующий Promises, может быть написан следующим образом:

checkEmailVerification() .then(function(emailComfirmed) { if (emailComfirmed) return getUserCats(userId); else throw new Error('Сначала, пожалуйста, подтвердите свой адрес электронной почты.'); }) .then(function(getArticlesList) { if (getArticlesList.length > 0) return getPopularArticlesUser(getArticlesList); else return getPopularArticles(); }) .then(function(getArticlesList) { console.log('Отображаемые статьи: ' + getArticlesList); }) .catch(failureCallback);
Code language: JavaScript (javascript)

Важно отметить, что эти последние примеры можно записать следующим образом (из ES6, с использованием стрелочных функций):

checkEmailVerification() .then(emailComfirmed => { if (emailComfirmed) return getUserCats(userId); else throw new Error('Сначала, пожалуйста, подтвердите свой адрес электронной почты.'); }) .then(getArticlesList => { if (getArticlesList.length > 0) return getPopularArticlesUser(getArticlesList); else return getPopularArticles(); }) .then(getArticlesList => console.log('Отображаемые статьи: ' + getArticlesList)) .catch(failureCallback);
Code language: JavaScript (javascript)

Итак, что же такое Обещания?

Обещания в JS являются именно такими. Они обещают, что что-то вот-вот произойдет, и мы, в скором времени, узнаем, успешно это произошло или нет.

С технической точки зрения:

Объект Promise представляет возможное завершение (или неудачу) асинхронной операции и ее результирующее значение.

В предыдущих примерах мы видели, как можно использовать обещания. Но мы еще не видели, как объявлять собственные объекты Promise.

Для каждого then в предыдущих примерах мы должны предположить, что выражение перед ним является или возвращает обещание.

Как создать обещание в JS

Объект Promise представляет значение, которое не обязательно известно на момент его создания.

Это представление позволяет нам выполнять действия, основанные на возвращаемом значении успеха или причины неудачи.

То есть, асинхронные методы создают значения, которые еще не доступны. Но сейчас идея заключается в том, что вместо ожидания и возврата конечного значения такие методы возвращают объект Promise (который предоставит нам результирующее значение в будущем).

В настоящее время многие JS-библиотеки обновляются, чтобы использовать Promises вместо простых функций обратного вызова.

Мы также можем создавать свои собственные обещания, основываясь на этом синтаксисе:

new Promise(function(resolve, reject) { ... });
Code language: JavaScript (javascript)

Этот конструктор в основном используется для обертывания функций, которые не поддерживают использование Promises.

  • Конструктор ожидает функцию в качестве параметра. Эта функция известна как executor.
  • Функция executor получает 2 аргумента: resolve и reject.
  • Функция executor выполняется непосредственно при выполнении объекта Promise, получая функции resolve и reject для соответствующего использования. Она вызывается еще до того, как конструктор Promise вернет созданный объект.
  • Функции resolve и reject при вызове “разрешают” или ” отклоняют” обещание. То есть, они изменяют состояние обещания (как мы уже видели, изначально оно находится в ожидании, но позже может быть выполнено или отклонено).
  • Обычно executor начинает некоторую асинхронную операцию, а после ее завершения вызывает функцию resolve для разрешения обещания или отказа, если произошло что-то непредвиденное.
  • Если функция executor выдает ошибку, обещание также отклоняется.
  • Значение, возвращаемое функцией executor, игнорируется.

Давайте рассмотрим пример.

Фразу “Я обещаю тебе, что через 3 секунды полюблю тебя”, можно перевести в код следующим образом:

var myPromise = new Promise(function(resolve, reject) { setTimeout(function() { resolve('Я люблю тебя'); }, 3000); });
Code language: JavaScript (javascript)

До этого момента, даже если мы не использовали функцию then для обещания, чтобы действовать с его результирующим значением, функция executor уже была выполнена.

Если вы используете интерактивную консоль браузера (DevTools в случае Chrome) и идете со мной, то теперь вы можете выполнить:

myPromise.then(function(value) { console.log(value); });
Code language: JavaScript (javascript)

И вы увидите, что результат (с которым разрешается обещание) доступен немедленно (без ожидания 3 секунд). Это доказывает то, о чем мы говорили раньше.

Но если вместо этого мы выполним все это:

var myPromise = new Promise(function(resolve, reject) { setTimeout(function() { resolve('Привет'); }, 3000); }); myPromise.then(function(value) { console.log(value); }); console.log(myPromise);
Code language: JavaScript (javascript)

Или то же самое:

const myPromise = new Promise((resolve, reject) => { setTimeout(() => resolve('Привет'), 3000); }); myPromise.then(value => console.log(value)); console.log(myPromise);
Code language: JavaScript (javascript)

Должно произойти следующее:

  • Функция executor выполняется, и создается наш объект Promise.
  • Мы вызываем метод then, указывая, что мы хотим сделать со значением, которое вернет обещание.
  • В консоль выводится [Object Promise].
  • Через 3 секунды после выполнения функции executor обещание разрешается, и мы выводим в консоль сообщение “Привет”.

Давайте рассмотрим последний пример.

Предположим, мы хотим считать до 3. Мы хотим выводить число в консоль через каждую секунду.

Одним из способов может быть:

setTimeout(() => { console.log(1); setTimeout(() => { console.log(2); setTimeout(() => { console.log(3); }, 1000); }, 1000); }, 1000);
Code language: JavaScript (javascript)

Возможно, вы решили создать 3 отдельных setTimeout, для 1000, 2000 и 3000 миллисекунд. Но допустим, что в данном случае это запрещено.

Как насчет создания функции, которая выполняет другую функцию за одну секунду?

function oneSecondLeft(anotherFunc) { setTimeout(anotherFunc, 1000); }
Code language: JavaScript (javascript)

Тогда у нас получится:

oneSecondLeft(() => { console.log(1); oneSecondLeft(() => { console.log(2); oneSecondLeft(() => console.log(3)); }); });
Code language: JavaScript (javascript)

Немного упростим, потому что мы не повторяем аргумент 1000 миллисекунд. Но callback hell все еще присутствует.

Как насчет выполнения обещания, которое разрешается за 1 секунду?

function waitForOneSecond() { return new Promise(function (resolve, reject) { setTimeout(resolve, 1000); }); }
Code language: JavaScript (javascript)

Обратите внимание, что мы возвращаем экземпляр обещания. Мы не инициализируем его напрямую (это привело бы к запуску executor, что не принесло бы нам пользы).

Последнее также можно записать следующим образом:

const waitForOneSecond = () => new Promise(resolve => setTimeout(resolve, 1000));
Code language: JavaScript (javascript)

Далее мы можем считать так:

waitForOneSecond() .then(() => { console.log(1); return waitForOneSecond(); }) .then(() => { console.log(2); return waitForOneSecond(); }) .then(() => { console.log(3); });
Code language: JavaScript (javascript)

Мы вызываем метод then из обещания. Далее возвращаем waitForOneSecond(), чтобы можно было составить цепочку обещаний.

Используя обещания много разных вещей. Все это вопрос практики и творчества.

Например, мы можем вернуть “обещание напечатать сообщение через 1 секунду”.

const printForOneSecond = (valor) => { return new Promise(resolve => { setTimeout(() => { console.log(valor); resolve(); }, 1000) }); };
Code language: JavaScript (javascript)

Затем мы постоянно повторяем его по цепочке:

printForOneSecond(1) .then(() => printForOneSecond(2)) .then(() => printForOneSecond(3));
Code language: JavaScript (javascript)

Важно использовать “then” в обещании, потому что так мы узнаем о его выполнении.

Возможно, вам интересно, сработает ли следующее:

printForOneSecond(1) .printForOneSecond(2) .printForOneSecond(3);
Code language: CSS (css)

Поскольку сейчас у нас определена функция printForOneSecond, это не сработает.

Почему?

Поскольку функция возвращает обещание у этого объекта Promise нет доступа к методу printForOneSecond.

Это как если бы мы выполняли следующее: new Promise(//).printForOneSecond(2). Вы можете проверить это сами.

Давайте посмотрим что можно сделать, если мы все таки хотим использовать конструкцию выше.

В реальном мире есть много факторов, которые мы не можем контролировать. Некоторые вещи происходят вообще без какой-либо логического основания.

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

Так что давайте попробуем.

Как заставить работать printForOneSecond(1).printForOneSecond(2).printForOneSecond(3)?

  • Идея заключается в том, что printForOneSecond(1) возвращает объект, который действительно имеет доступ к методу printForOneSecond.
  • Альтернативным решением является определение класса, в котором доступен этот метод.
class Printer extends Promise { printForOneSecond(value) { return this.then(() => this.returnPromise(value)); } returnPromise(value) { return new Printer(resolve => { setTimeout(() => { console.log(value); resolve(); }, 1000) }) } }
Code language: JavaScript (javascript)

Мы создаем класс Printer из класса Promise.

Поскольку метод printForOneSecond теперь вызывает метод then обещания, мы можем использовать его таким образом:

Printer.resolve().printForOneSecond(1).printForOneSecond(2).printForOneSecond(3);
Code language: CSS (css)

Почему мы начали с resolve()?

Мы делаем это потому, что если мы используем new Printer(), то получим ошибку (наследуя от Promise, класс Printer вынужден получать executor в своем конструкторе).

Можем ли мы переопределить конструктор? Да, но если есть наследование классов, вы в любом случае должны вызвать конструктор родительского класса, используя super (это правило Javascript, и вы не можете его игнорировать).

Получается, мы не можем заставить printForOneSecond(1).printForOneSecond(2).printForOneSecond(3) работать самостоятельно?

Можно выстраивать цепочки обещаний без явного использования then. Хотя следует помнить, что нет ничего плохого в том, чтобы их использовать. На самом деле, к этому стоит привыкнуть.

Решение в последней ссылке требует большой изобретательности и не совсем практично. Но я делюсь этим, чтобы вы увидели, что все возможно в таком гибком языке, как Javascript..

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *