Как сделать промис синхронным

Обновлено: 08.07.2024

При стандартном выполнении JavaScript инструкции выполняются последовательно, одна за другой. То есть сначала выполняется первая инструкция, потом вторая и так далее. Однако что, если одна из этих операций выполняется продолжительное время. Например, она выполняет какую-то высоконагруженную работу, как обращение по сети или обращение к базе данных, что может занять неопределенное и иногда продолжительное время. В итоге при последовательном выполнении все последующие операции будут ожидать выполнения этой операции. Чтобы избежать подобной ситуации JavaScript предоставляет ряд инструментов, которые позволяют избежать подобного сценария, чтобы последующие операции могли выполняться, пока выполняется продолжительная операция. Одним из таким инструментов являются промисы (promise).

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

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

pending (состояние ожидания): начальное состояние, промис создан, но выполнение еще не завершено

fulfilled (успешно завершено): действие, которое представляет промис, успешно завершено

rejected (завершено с ошибкой): при выполнении действия, которое представляет промис, произошла ошибка

Для создания промиса применяется конструктор типа Promise :

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

При создании промиса, когда его функция еще не начала выполняться, промис переходит в состояние "pending", то есть ожидает выполнения.

Как правило, функция, которая передается в конструктор Promise, принимает два параметра:

Оба этих параметра - resolve и reject также представляют функции. И каждая из этих функций принимает параметр любого типа.

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

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

Успешное выполнение промиса

Итак, первый параметр функции в конструкторе Promise - функция resolve выполняется при успешном выполненим. В эту функцию обычно передается значение, которое представляет результат операции при успешном выполнении. Это значение может представлять любой объект. Например, передадим в эту функцию строку:

Функция resolve() вызывается в конце выполняемой операции после всех действий. При вызове этой функции промис переходит в состояние fulfilled (успешно выполнено).

При этом стоит отметить, что теоретически мы можем возвратить из функции результат, но практического смысла в этом не будет:

Данное возвращаемое значение мы не сможем передать во вне. И если действительно надо возвратить какой-то результат, то он передается в функцию resolve() .

Передача информации об ошибке

Второй параметр функции в конструкторе Promise - функция reject вызывается при возникновении ошибки. В эту функцию обычно передается некоторая информация об ошибке, которое может представлять любой объект. Например:

При вызове функции reject() промис переходит в состояние rejected (завершилось с ошибкой).

Объединение resolve и reject

Естественно мы можем определить логику, при которой в зависимости от условий будут выполняться обе функции:

В данном случае, если значени константы y равно 0, то сообщаем об ошибке, вызывая функцию reject() . Если не равно 0, то выполняем операцию деления и передаем результат в функцию resolve() .

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

Введение

Концепция 1: Как создать промис и применить его

Рассмотрим следующую асинхронную функцию:

Чтобы async_fn вернула результат своей работы, можно:

  1. Использовать callback-функцию.
  2. Использовать промис.

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

Синтаксис создания нового промиса:

Класс Promise предоставляет три метода:

  1. then : если промис выполняется успешно, то будет осуществлен обратный вызов метода then.
  2. catch : если промис не выполняется, будет осуществлен обратный вызов метода catch.
  3. finally : метод вызывается, если промис выполнен.

Пример использования промиса:

Концепция 2: Цепочка промисов

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

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

Цепочка вызовов может быть реализована двумя способами:

  • Использование отдельного обработчика ошибок для каждого успешного обратного вызова – при этом метод then принимает два аргумента: один для успешного обратного вызова, а другой — для неудачного :
  • Использование стандартного обработчика ошибок – обратный вызов failureHandler не является обязательным в методе then. Поэтому можно использовать блок catch для общей обработки ошибок:

Концепция 2: Цепочка промисов

В приведенном выше примере можно передать параметр через разные обратные вызовы и увеличивать их значение.

Если мы возвращаем error из любого обратного вызова then, то для последующих методов then обратный вызов error будет выполнен. После чего ошибка будет передана в последний блок catch .

Концепция 3: Совместное выполнение нескольких промисов

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

  • Дождитесь разрешения всех промисов: для этого объект Promise предоставляет метод all. Он принимает один или несколько промисов в виде массива и возвращает промис, который разрешается, когда все они выполняются успешно или один из них отклоняется:

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

Концепция 3: Совместное выполнение нескольких промисов

  • Дождитесь решения хотя бы одного промиса из списка. Чтобы решить родительский промис, как только выполнится любой из дочерних промисов, можно использовать метод race объекта Promise.

Концепция 3: Совместное выполнение нескольких промисов - 2

Концепция 4: Async и await

Async-await позволяет вызывать асинхронные функции синхронно. Рассмотрим этот момент более подробно.

Асинхронные функции – это функции, которые переходят в состояние ожидания при вызове, а разрешение зависит от результата выполнения.

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

Предположим, что у нас есть две асинхронные функции:

  • getManagerByEmployeeId : принимает employeeId в качестве входных параметров и возвращает managerId.
  • getManagerNameById: принимает в качестве входных параметров managerId и возвращает имя менеджера.

Наша задача – получить имя менеджера для данного идентификатора сотрудника. То есть, реализовать функцию getManagerName в приведенном ниже коде.

Один из способов сделать это – использовать структуру вложенных промисов и преобразовать getManagerName в промис.

Это создаст структуру вложенного кода, которая сложна для понимания и заставляет использовать промис в функции getManagerName .

Теперь рассмотрим реализацию концепции async-await .

Благодаря ей код стал проще и нам не нужно использовать промис в функции getManagerName.

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

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

Но отклоненный промис выдает ошибку при вызове функции. Поэтому его нужно обработать с помощью блока try-catch, окружающего вызов асинхронной функции.

Концепция 5: Использование разрешенных промисов

Во время модульного тестирования иногда нужно имитировать действие фактического промиса. Мы можем сделать это, используя уже разрешенные промисы с помощью методов resolve и reject класса Promise .

Заключение

Чтобы узнать о промисах еще больше, изучите соответствующую документацию MDN.

Пожалуйста, опубликуйте ваши мнения по текущей теме материала. За комментарии, подписки, дизлайки, отклики, лайки огромное вам спасибо!

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

Обратные вызовы являются основой асинхронного программирования на JavaScript и в Node.js, но за прошедшие годы появились альтернативные подходы, позволяющие упростить работу с асинхронным кодом.

В этой статье мы рассмотрим самые популярные из таких альтернатив, объекты Promise и генераторы, а также новейший синтаксис async await, который будет введен в JavaScript как часть спецификации ECMAScript 2017.

Мы увидим, как эти альтернативы могут упростить управление асинхронными потоками. И наконец, сравним все эти подходы, выявив плюсы и минусы каждого из них, чтобы иметь возможность разумно подойти к выбору подхода, наилучшим образом соответствующего требованиям проекта на платформе Node.js.

Объект Promise в JavaScript и Node.js

Стиль передачи продолжения (Continuation Passing Style, CPS) не является единственным способом реализации асинхронного кода. В действительности экосистема JavaScript предлагает интересные альтернативы традиционному шаблону обратных вызовов. Одной из самых распространенных таких альтернатив является объект Promise, которому уделяется все больше внимания, особенно сейчас, когда он стал частью спецификации ECMAScript 2015 и обрел встроенную поддержку, начиная с версии 4, платформы Node.js.

Что такое и для чего нужен Promise

Выражаясь простым языком, объект Promise является абстракцией, позволяющей функциям возвращать объект Promise, представляющий конечный результат асинхронной операции. Мы говорим, что объект Promise ожидает, если асинхронная операция еще не завершилась, выполнен – если операция завершилась успешно, и отклонен – если возникла ошибка. После того как объект Promise будет выполнен или отклонен, он считается установившимся.

Чтобы получить результат выполнения или ошибку (причину), вызвавшую отклонение, можно использовать метод then() объекта Promise:

Здесь onFulflled() – это функция, которой передается результат выполнения асинхронной операции, а onRejected() – функция, которой передается причина отклонения. Обе функции являются необязательными.

Чтобы получить представление, как применение объектов Promise может изменить код, рассмотрим следующий фрагмент кода:

Объекты Promise позволяют преобразовать этот типичный CPS-­код в более структурированный и элегантный код, например:

Одним из важнейших свойств метода then() является синхронный возврат другого объекта Promise. Если любая из функций – onFulflled() или onRejected() – вернет значение x, метод then() вернет один из следующих объектов Promise:

  • выполненный со значением x, если x является значением;
  • выполненный с объектом x, где x является объектом Promise или thenableобъектом;
  • отклоненный с причиной отклонения x, где x является объектом Promise или thenable­объектом.

thenable-объект – это Promise-подобный объект, имеющий метод then(). Этот термин используется для обозначения объекта, фактическая реализация которого отличается от реализации Promise.

Эта особенность позволяет создавать цепочки из объектов Promise, облегчая объединение и компоновку асинхронных операций в различных конфигурациях. Кроме того, если не указывается обработчик onFulflled() или onRejected(), результат или причина отклонения автоматически направляется следующему объекту Promise в цепочке. Это дает возможность, например, автоматически передавать ошибку вдоль всей цепочки, пока она не будет перехвачена обработчиком onRejected(). Составление цепочек объектов Promise делает последовательное выполнение заданий тривиальной операцией:

Схема на рисунке иллюстрирует другую точку зрения на работу цепочки объектов Promise:

объект Promise

Другим важным свойством объектов Promise является гарантированный асинхронный вызов функций onFulflled() и onRejected(), даже при синхронном выполнении, как в предыдущем примере, где последняя функция then() в цепочке возвращает строку ‘done’. Такая модель поведения защищает код от непреднамеренного высвобождения Залго, что без дополнительных усилий делает асинхронный код более последовательным и надежным.

А теперь самое интересное: если в обработчике onFulflled() или onRejected() возбудить исключение (оператором throw), возвращаемый методом then() объект Promise автоматически будет отклонен с исключением в качестве причины отказа. Это огромное преимущество перед CPS, потому что исключение автоматически будет передаваться вдоль по цепочке, а это означает, что можно использовать оператор throw.

Исторически сложилось, что существует множество библиотек, реализующих объекты Promise, большинство которых не совместимо друг с другом, что препятствует созданию then­цепочек из объектов Promise, созданных разными библиотеками.

Сообщество JavaScript провело сложную работу по преодолению этого ограничения, в результате была создана спецификация Promises / A+. Эта спецификация детально описывает поведение метода then и служит основой, обеспечивающей возможность взаимодействий между объектами Promise из различных библиотек.

Реализации Promises/A+

Как в JavaScript, так и в Node.js есть несколько библиотек, реализующих спецификацию Promises/A+. Ниже перечислены наиболее популярные из них:

По существу, они отличаются только наборами дополнительных возможностей, не предусмотренных стандартом Promises/A+. Как упоминалось выше, этот стандарт определяет модель поведения метода then() и процедуру разрешения объекта Promise, но не регламентирует других функций, например порядка создания объекта Promise на основе асинхронной функции с обратным вызовом.

В примерах ниже мы будем использовать методы, поддерживаемые объектами Promise стандарта ES2015, поскольку они доступны в Node.js, начиная с версии 4, и не требуют подключения внешних библиотек.

Для справки ниже перечислены методы объектов Promise, определяемые стандартом ES2015.

Конструктор (new Promise(function(resolve, reject) <>)): создает новый объект Promise, который разрешается или отклоняется в зависимости от функции, переданной в аргументе. Конструктору можно передать следующие аргументы:

  • resolve(obj): позволяет разрешить объект Promise и вернуть результат obj, если obj является значением. Если obj является другим объектом Promise или thenable­объектом, результатом станет результат выполнения obj;
  • reject(err): отклоняет объект Promise с указанной причиной err. В соответствии с соглашением err должен быть экземпляр Error.

Статические методы объекта Promise:

  • Promise.resolve(obj): возвращает новый объект Promise, созданный из thenableобъекта, если obj – thenable­объект, или значение, если obj – значение;
  • Promise.all(iterable): создает объект Promise, который разрешается результатами выполнения, если все элементы итерируемого объекта iterable выполнились, и отклоняется при первом же отклонении любого из элементов. Любой элемент итерируемого объекта может быть объектом Promise, универсальным thenable­объектом или значением;
  • Promise.race(iterable): возвращает объект Promise, разрешаемый или отклоняемый, как только разрешится или будет отклонен хотя бы один из объектов Promise в итерируемом объекте iterable, со значением или причиной этого объекта Promise.

Методы экземпляра Promise:

  • promise.then(onFulflled, onRejected): основной метод объекта Promise. Его модель поведения совместима со стандартом Promises/A+, упомянутым выше;
  • promise.catch(onRejected): удобная синтаксическая конструкция, заменяющая promise.then(undefned, onRejected).

Стоит отметить, что некоторые реализации предлагают другой асинхронный механизм – механизм отложенных вычислений. Мы не будем рассматривать его, поскольку он не является частью стандарта ES2015.

Перевод функций в стиле Node.js на использование объектов Promise

В JavaScript не все асинхронные функции и библиотеки поддерживают объекты Promise изначально. Обычно типичные функции, основанные на обратных вызовах, требуется преобразовать так, чтобы они возвращали объекты Promise. Этот процесс называется переводом на использование объектов Promise.

К счастью, соглашения об обратных вызовах, используемые на платформе Node.js, позволяют создавать функции, способные переводить любые функции в стиле Node.js на использование объектов Promise. Это несложно осуществить с помощью конструктора объекта Promise. Создадим новую функцию promisify() и добавим ее в модуль utilities.js (чтобы ее можно было использовать в приложении веб­паука):

Приведенная выше функция возвращает другую функцию – promisifed(), которая является версией callbackBasedApi, возвращающей объект Promise. Вот как она работает:

  1. функция promisifed() создает новый объект с помощью конструктора Promise и немедленно возвращает его;
  2. в функции, что передается конструктору Promise, мы передаем специальную функцию обратного вызова для вызова из callbackBasedApi. Поскольку функция обратного вызова всегда передается в последнем аргументе, мы просто добавляем ее в список аргументов (args) функции promisifed();
  3. если специальная функция обратного вызова получит ошибку, объект Promise немедленно отклоняется;
  4. в случае отсутствия ошибки осуществляется разрешение объекта Promise со значением или массивом значений, в зависимости от количества результатов, переданных функции обратного вызова;
  5. в заключение вызывается callbackBasedApi с созданным списком аргументов.

Большинство реализаций поддерживает вспомогательный метод преобразования типичных функций в стиле Node.js в функции, возвращающие объекты Promise. Например, библиотека Q содержит функции Q.denodeify() и Q.nbind(), библиотека Bluebird имеет Promise.promisify(), а When.js содержит node.lift().

Последовательное выполнение Промисов

Теперь, после знакомства с теорией, можно приступать к созданию приложений. Рассмотрим работу на примере создание веб-паука:

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

Promise: Последовательные итерации

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

Для асинхронного обхода всех ссылок на веб-­странице нужно динамически создать цепочку объектов Promise.

В конце цикла переменная promise будет содержать объект Promise, который вернул последний вызов then() в цикле, поэтому он будет разрешен после разрешения всех объектов Promise в цепочке.


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

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

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

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

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

Асинхронная функция определяется функциональным выражением c ключевым словом async . Базовая функция выглядит так:

Мы определяем функцию как асинхронную, помещая перед декларацией функции async . Это ключевое слово может использоваться с любым синтаксисом объявления функции:

Как только мы определили функцию как асинхронную, мы можем использовать ключевое слово await . Это ключевое слово помещается перед вызовом промиса, что приостанавливает выполнение функции до тех пор, пока промис не будет выполнен (fulfilled) или отклонен (rejected).

Обработка ошибок в асинхронных функциях выполняется с помощью блоков try и catch . Первый блок, try , позволяет нам попытаться произвести действие. Второй блок, catch , вызывается, если действие произвести не удалось. Он принимает один параметр, содержащий выброшенную ошибку.

Асинхронные функции не являются заменой промисов. Они работают сообща. Асинхронная функция ожидает ( await ) исполнения промиса и всегда возвращает промис.

Промис, возвращаемый асинхронной функцией, будет разрешен (resolve) с тем значением, которое вернет функция.

Если будет выдана ошибка, промис будет отклонен (rejected) с этой ошибкой.

С промисами мы можем выполнять несколько промисов параллельно с помощью метода Promise.all() .

С асинхронными функциями нам нужно немного поработать, чтобы получить тот же эффект. Если мы просто перечислим функции, ожидающие в последовательности, они будут выполняться последовательно, так как await приостанавливает выполнение остальной части функции.

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

Это займет всего 500 миллисекунд, потому что обе функции pause500ms() выполняются одновременно.

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

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

  1. Ожидание 1000 миллисекунд
  2. Вывод “foo complete!”
  3. Вывод “bar complete!”

На момент написания статьи асинхронные функции и промисы доступны в текущих версиях всех основных браузеров, за исключением Internet Explorer и Opera Mini.

Нативные промисы являются одним из самых больших изменений, внесенных ES2015 в JavaScript. Они устраняют некоторые наиболее существенные проблемы с функциями обратного вызова и позволяют нам писать асинхронный код, в большей степени соблюдающий синхронную логику.

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

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

По итогам этой статьи вы сможете:

  • Сформулировать, почему у нас есть проблемы и какие проблемы решают промисы;
  • Объяснить, что такое промисы, как их имплементацию, так и использование;
  • Реализовать с помощью промисов распространенные паттерны функций обратного вызова.

Одно примечание. Примеры подразумевают запуск Node. Вы можете копировать и вставлять код вручную или просто клонировать репозиторий.

Итак, клонируете репозиторий и переходите на ветку Part_1 :

Вы на пути к истине. Наш маршрут включает в себя следующие вопросы:

  • Проблема функций обратного вызова
  • Промисы: определения и замечания из спецификации
  • Промисы и не-инверсия управления
  • Управление потоком с промисами
  • Осознаем смысл then , reject и resolve

Асинхронность

Если вы достаточно времени работали с JavaScript, то вы уже слышали, что он фундаментально неблокирующий или асинхронный. Но что это означает?

Синхронный и асинхронный код

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

Выполнение синхронного кода в консоли

Асинхронный код прямо противоположен: он позволяет выполнять остальные части программы, пока сам занят обработкой долго текущих операций, таких как ввод/вывод или сетевые запросы. Он также называется неблокирующий код. Вот асинхронный аналог предыдущего фрагмента:

Так как readFile не блокирует, этот метод обязан немедленно вернуться для того, чтобы программа продолжала выполняться. Так как немедленно это явно недостаточно для выполнения операций ввода-вывода, метод возвращает undefined и мы выполняем дальнейшую программу настолько, насколько мы можем сделать без выполнения readFile … После этого мы считываем файл.

Вопрос в том, как мы можем узнать, что чтение завершено?

К сожалению, никак. Но readFile может. В вышестоящем фрагменте кода мы передали readFile два аргумента: имя файла и функцию, называемую функция обратного вызова (callback), которую мы хотим выполнить сразу после завершения чтения файла.

Работает это примерно так: readFile смотрит,что находится внутри $/$ , а программа занимается своими делами. Как только readFile узнает, что там, он выполняет callback с contents в качестве аргумента, а в случае ошибки возвращает error .

Важно уяснить: мы не можем знать, когда содержимое файла будет готово — только readFile может. Поэтому мы передаем ему функцию обратного вызова и доверяем, что он использует ее правильно.

Это общий паттерн для работы с асинхронными функциями: вызываете их с параметрами и передаете им функцию обратного вызова для ее выполнения с полученным результатом.

Функции обратного вызова это работающее решение, но не идеальное. У них есть две большие проблемы:

  1. Инверсия управления
  2. Сложная обработка ошибок

Инверсия контроля

Первая проблема это проблема доверия.

Когда мы передаем readFile нашу функцию обратного вызова, мы верим, что она будет вызвана. И у нас нет совершенно никаких гарантий этого. Также как нет гарантий, что при вызове ей будут переданы надлежащие параметры, в правильном порядке и нужное количество раз.

На практике это, конечно, не столь фатально: мы пишем функции обратного вызова почти 20 лет и до сих пор не поломали интернет. И в данном случае, мы знаем, что достаточно безопасно полагаться на код ядра Node.

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

Неявная обработка ошибок

В синхронном коде вы можете использовать try / catch / finally для обработки ошибок.

Асинхронный код пытается, конечно, но…

Это не работает так, как ожидается. Потому что блок try оборачивает readFile , который всегда успешно возвращает undefined . В такой ситуации у try всегда будет без происшествий.

Единственный способ для readFile сообщить вам об ошибках — это передать их в вашу функцию обратного вызова, где вы сами обработаете их.

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

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

Промисы

Представьте, вы только что заказали весь каталог You Don’t Know JS от O’Reilly. За ваши с трудом заработанные деньги они прислали расписку, что в следующий понедельник вы получите новенькую стопку книг. До этого счастливого понедельника никаких книг у вас не будет — но вы верите, что они появятся, так как вам пообещали (promise) прислать их.

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

Конечно, через несколько дней O’Reilly может сообщить о том, что с понедельником не судьба и книги будут чуток позже, другими словами, нужное значение будет в будущем. Вы относитесь к промису, как к ожидаемому значению и пишете код так, как будто оно уже у вас есть.

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

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

Жизненный цикл промиса: краткий обзор состояний

Представьте, что вы используете промис для вызова API.

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

Как только сервер ответил, у нас есть два возможных исхода:

  1. Промис получает ожидаемое значение, значит, он выполнен (fulfilled). Ваши книжки пришли.
  2. Где-то по ходу выполнения произошла ошибка, промис отклонен (rejected). Вы получили уведомление о том, что никаких книжек не будет.

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

Теперь, когда мы разобрались с основными понятиями, посмотрим, как это все использовать.

Фундаментальные методы промисов

Процитирую спецификацию с Promises/A+:

В этом разделе мы ближе рассмотрим базовое использование промисов:

  1. Создание промисов с конструктором;
  2. Обработка успешного результата с resolve ;
  3. Обработка ошибок с reject ;
  4. Настройка управления потоком с then и catch .

В нашем примере мы будем использовать промисы для очистки кода нашей функции fs.readFile .

Создание промисов

Самый простой способ это создание промисов непосредственно с помощью конструктора.

Учтите, что мы передаем конструктору промиса функцию в качестве аргумента. Именно здесь мы сообщаем промису, как выполнять асинхронную операцию; что делать, когда мы получим то, что ожидаем и что делать в случае ошибки. В частности:

  1. Аргумент resolve это функция, инкапсулирующая то, что мы хотим сделать при получении ожидаемого значения. Когда мы получаем ожидаемое значение ( val ), мы передаем его resolve в качестве аргумента: resolve(val) .
  2. Аргумент reject это тоже функция, представляющая наши действия в случае получения ошибки. Если мы получим ошибку ( err ), мы вызовем reject с ней: reject(err) .
  3. Наконец, функция, переданная нами в конструктор промиса, обрабатывает сам асинхронный код. Если она возвращает ожидаемый результат, мы вызываем resolve с полученным значением. Если она выбрасывает ошибку, мы вызываем reject с этой ошибкой.

В нашем примере мы обернем fs.readFile в промис. Как должны выглядеть наши resolve и reject ?

  1. При успехе мы хотим вызвать console.log для вывода содержимого файла.
  2. При неудаче мы поступим аналогично: выведем ошибку в консоль.

Таким образом мы получим следующее:

Затем нам надо написать функцию, которую мы передаем конструктору. Запомните, нам нужно сделать следующее:

  1. Прочитать файл;
  2. В случае успеха выполнить resolve с его содержимым;
  3. При неудаче выполнить reject с полученной ошибкой.

Итак, технически все сделано: этот код создает промис, который делает именно то, что нам надо. Но если мы запустим этот код, вы заметите, что он выполняется без вывода результата или ошибки.

Она дала обещание, а затем…

Проблема в том, что мы написали наши методы resolve и reject , но на самом деле не передали их в промис. Для того, чтобы сделать это, нам надо ознакомиться с еще одной базовой функцией для управления потоком на основе промисов: then (затем).

Каждый промис обладает методом then , принимающим две функции в качестве аргументов: resolve и reject , именно в таком порядке. Вызов then в промисе и передача ему этих двух функций, делает их доступными для конструктора промиса.

Так промис прочитает файл и вызовет написанный нами метод resolve в случае успеха.

Важно запомнить, что then всегда возвращает объект-промис. Это значит, что вы можете сделать цепочку из нескольких вызовов then для создания сложного и синхронно-выглядящего потока над асинхронными операциями. В следующей статье мы обсудим это подробнее, а как это выглядит, мы поймем разбирая пример catch .

Синтаксический сахар для обработки ошибок

Мы передали then две функции: resolve , для вызова в случае успеха и reject на случай ошибки.

У промисов также есть функция похожая на then , называемая catch . Она принимает обработчик reject в качестве единственного аргумента.

Так как then всегда возвращает промис, в нашем примере мы можем только передать then обработчик resolve , а после этого подключить в цепочку catch с обработчиком reject .

Наконец, стоит упомянуть, что catch(reject) это всего лишь синтаксический сахар для then(undefined, reject) . То есть мы можем также написать:

Но такой код будет менее читаемым.

Заключение

Промисы это незаменимый инструмент для асинхронного программирования. Они могут напугать поначалу, но только пока вы с ними незнакомы: используйте их пару раз и они станут такими же естественными для вас как if / else .

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

В качестве дополнительной литературы ознакомьтесь со статьей Доменика Дениколы States and Fates, чтобы овладеть терминологией и с главой Кайла Симпсона о промисах из той стопки книг, на примере которой мы разбирали промисы.

Читайте также: