Про обещания (promises) уже много написано. Эта статья — просто попытка собрать наиболее необходимые на практике приемы использования обещаний с достаточно подробными пояснениями того, как это работает. Сначала несколько определений. Обещания (promises) — это объекты, позволяющие упорядочить выполнение асинхронных вызовов. Асинхронный вызов — это вызов функции, при котором выполнение основного потока кода не дожидается завершения вызова. Например, выполнение http-запроса не прерывает выполнение основного потока. То есть выполняется запрос, и сразу, не дожидаясь его завершения, выполняется код следующий за этим вызовом, а результат http-запроса обрабатывается после его завершения функцией обратного вызова (callback-функцией). Далее будем последовательно разбираться с функционированием обещаний. Пока будем исходить из того, что у нас уже есть объект-обещание выполнить некий асинхронный вызов. О том, откуда берутся обещания и, как их сформировать самому, поговорим чуть позже. 1. Обещания предоставляют механизм, позволяющий управлять последовательностью асинхронных вызовов. Иными словами, обещание — это просто обертка над асинхронным вызовом. Обещание может разрешиться успешно или завершиться с ошибкой. Суть механизма в следующем. Каждое обещание предоставляет две функции: then() и catch(). В качестве аргумента в каждую из функций передается функция обратного вызова. Callback-функция в then вызывается в случае успешного разрешения обещания, а в catch — в случае ошибки: 2. Следующий момент, который нужно уяснить. Обещание разрешается только один раз. Иными словами асинхронный вызов, который происходит внутри обещания выполняется только один раз. В дальнейшем обещание просто возвращает сохраненный результат асинхронного вызова, больше его не выполняя. Например, если мы имеем некоторое обещание promise, то: 3. Функции then() и catch() возвращают обещания. Таким образом, можно выстраивать цепочки обещаний. При этом результат, который возвращает callback-функция в then(), передается в качестве аргумента на вход callback-функции последующего then(). А аргумент вызова throw(message) передается в качестве аргумента callback-функции в catch(): 4. В качестве результата callback-функция в then() может возвращать не только какое-либо значение, но и другое обещание. При этом в следующий вызов then() будет передан результат разрешения этого обещания: 5. Можно то же самое сделать, выстроив разрешение обещаний в одну линейную цепочку без вложений: Обратите внимание на то, что каждая цепочка обещаний заканчивается вызовом catch(). Если этого не делать, то возникающие ошибки будут пропадать в недрах обещаний и, тогда невозможно будет установить, где произошла ошибка. Если необходимо выполнить последовательность асинхронных вызовов (то есть, когда каждый следующий вызов должен выполниться строго после завершения предыдущего), то эти асинхронные вызовы необходимо выстроить в цепочку обещаний.  Обещания можно получить двумя способами: либо функции используемой библиотеки могут возвращать готовые обещания, либо можно самостоятельно оборачивать в обещания асинхронные вызовы функций, не поддерживающих обещания (промисификация). Рассмотрим каждый вариант отдельно. Многие современные библиотеки поддерживают работу с обещаниями. О том, поддерживает библиотека обещания или нет, можно узнать из документации. Некоторые библиотеки поддерживают оба варианта работы с асинхронными вызовами: функции обратного вызова и обещания. Рассмотрим несколько примеров из жизни. В документации приводится такой пример работы с функциями библиотеки (на примере выборки данных): Кажется, что всё красиво. Однако, если после выборки данных и их обработки нужно сделать еще какие-то операции с данными, например, обновление записей в одной таблице, потом вставка новых записей в другой таблице, то вложенность функций обратного вызова становится такой, что читаемость кода резко начинает стремиться к нулю: Даже при отсутствии основного кода в приведенном примере уже понимать написанное очень трудно. А если запросы к базе данных нужно еще выполнить в цикле, то при использовании функций обратного вызова задача вообще становится неразрешимой. Однако, функции библиотеки Waterline ORM умеют работать с обещаниями, хотя об этом в документации упоминается как-то вскользь. Но использование обещаний сильно упрощает жизнь. Оказывается, что функции запросов к базе данных возвращают обещания. А это значит, что последний пример на языке обещаний можно записать так: Думаю, что нет необходимости говорить, какое решение лучше. Особенно приятно то, что обработка всех ошибок теперь делается в одном месте. Хотя нельзя сказать, что это всегда хорошо. Иногда бывает так, что из контекста ошибки не ясно в каком именно блоке она произошла, соответственно отладка усложняется. В этом случае никто не запрещает вставлять вызов catch() в каждое обещание: Даже в этом случае код более понятный, чем в случае использования функций обратного вызова. Как и в предыдущем случае, примеры из документации используют функции обратного вызова: Мы уже увидели на предыдущем примере, что использование обратных вызовов сильно усложняет жизнь в том случае, когда нужно сделать много последовательных асинхронных вызовов. К счастью, при внимательном изучении документации, можно узнать, что, если в функции этой библиотеки не передавать в качестве аргумента callback-функцию, то она вернёт обещание. А значит, предыдущий пример можно записать так: Особенность работы с данной библиотекой является то, что для выполнения любого запроса к базе используется объект-дескриптор базы данных db, возвращаемый в результате асинхронного вызова connect(). То есть, если нам в процессе работы необходимо выполнить множество запросов к базе данных, то возникает необходимость каждый раз получать этот дескриптор. И здесь использование обещаний красиво решает эту проблему. Для этого нужно просто сохранить обещание подключения к базе в переменной: Что хорошо при такой организации кода, так это то, что соединение с базой данных устанавливается один раз (напоминаю, что обещание разрешается только один раз, далее просто возвращается сохраненный результат). Однако в приведенном выше примере есть одна проблема. После завершения всех запросов к базе нам нужно закрыть соединение с базой. Закрытие соединения с базой можно было бы вставить в конце цепочки обещаний. Однако мы не знаем, какая из двух запущенных нами цепочек завершится раньше, а значит, не понятно, в конец какой из двух цепочек вставлять закрытие соединения с базой. Решить эту проблему помогает вызов Promise.all(). О нем поговорим чуть позже. Так же пока отложим обсуждение вопроса о том, как с помощью обещаний можно организовывать циклическое выполнение асинхронных вызовов. Ведь как было сказано выше использование обратных вызовов не позволяет решить эту проблему вообще. А сейчас перейдем к рассмотрению вопроса о том, как самостоятельно создавать обещания, если библиотека их не поддерживает. Часто бывает и так, что библиотека не поддерживает обещания. И предлагает использовать только обратные вызовы. В случае небольшого скрипта и простой логики можно довольствоваться и этим. Но если логика сложна, то без обещаний не обойтись. А значит, нужно уметь оборачивать асинхронные вызовы в обещания. В качестве примера рассмотрим оборачивание в обещание асинхронной функции получения значения атрибута из хранилища redis. Функции redis-библиотеки не поддерживают обещания и работают только с функциями обратного вызова. То есть, стандартный вызов асинхронной функции будет выглядеть так: Теперь обернем эту функцию в обещание. JavaScript предоставляет для этого объект Promise. То есть, для создания нового обещания нужно сделать так: В качестве аргумента конструктору Promise передается функция, внутри которой и происходит асинхронный вызов. Аргументами этой функции являются функции resolve и reject. Функция resolve должна быть вызвана в случае успешного завершения асинхронного вызова. На вход функции resolve передается результат асинхронного вызова. Функция reject должна вызываться в случае ошибки. На вход функции reject передается ошибка. Соединив все вместе получим функцию get(attr), которая возвращает обещание получить параметр из хранилища redis: Вот и все. Теперь можно спокойно пользоваться нашей функцией get() привычным для обещаний образом: У любого здравомыслящего человека этот вопрос обязательно появится. Ведь чем мы сейчас занимались? Мы занимались разъяснением того, как последовательно выполнять (асинхронные) вызовы функций. В любом "нормальном" (не работающим с асинхронными вызовами) языке программирования это делается просто последовательной записью инструкций. Не нужны никакие обещания. А тут столько мучений ради того, чтобы сделать простую последовательность вызовов. Может асинхронные вызовы вообще не нужны? Ведь из-за них столько проблем! Но нет худа без добра. При наличии асинхронных вызовов мы можем выбирать способ выполнения вызовов: последовательно или параллельно. То есть если одно действие требует для своего выполнения наличия результата другого действия, то мы используем вызовы then() в обещаниях. Если же выполнение нескольких действий не зависит друг от друга, то мы можем запустить эти действия на параллельное выполнение. Как запустить параллельное выполнение нескольких независимых действий? Для этого используется вызов: То есть, на вход функции Promise.all передается массив обещаний. Все обещания в этом случае запускаются на выполнение сразу. Функция Promise.all возвращает обещание выполнить все обещания, поэтому вызов then() в этом случае срабатывает после выполнения всех обещаний массива. Параметр results, который передается на вход функции в then(), является массивом результатов работы обещаний в той последовательности, в которой они передавались на вход Promise.all. Конечно можно обещания и не оборачивать в Promise.all, а просто последовательно запустить на выполнение. Однако, в этом случае мы теряем контроль над моментом завершения выполнения всех обещаний. Часто возникает задача последовательного выполнения обещаний в цикле. Здесь мы будем рассматривать именно последовательное выполнение обещаний, так как, если обещания допускают параллельное выполнение, то нет ничего проще составить в цикле (или с помощью функций map, filter и т.п.) массив обещаний и передать его на вход Promise.all. Как же в цикле последовательно выполнить обещания? Ответ простой — никак. То есть, строго говоря, обещания в цикле выполнить нельзя, но в цикле можно составить цепочку обещаний, выполняющихся одно за другим, что по сути эквивалентно цикличному выполнению асинхронных действий. Перейдем к рассмотрению самих механизмов цикличного построения цепочек обещаний. Будем отталкиваться от привычных всем программистам разновидностей циклов: цикл for и цикл forEach. Предположим, что некая функция doSomething() возвращает обещание выполнить какое-то асинхронное действие, и нам надо выполнить это действие последовательно n раз. Пусть само обещание возвращает некий строковый результат result, который используется при следующем асинхронном вызове. Также допустим, что первый вызов делается с пустой строкой. В этом случае выстраивание цепочки обещаний делается так: В этом примере-шаблоне пояснения требует только первая строка. Остальное должно быть понятно из рассмотренного выше материала. Функция Promise.resolve() возвращает успешно разрешенное обещание результатом которого является аргумент этой функции. То есть Promise.resolve("") равносильно следующему: Предположим, нам нужно выполнить какое-либо асинхронное действие для каждого элемента массива array. Пусть это асинхронное действие обернуто в некую функцию doSomething(), которая возвращает обещание выполнить это асинхронное действие. Шаблон того, как в этом случае выстроить цепочку обещаний, как это ни странно, основан не на использовании функции forEach, а на использовании функции reduce. Для начала рассмотрим, как работает функция reduce. Эта функция предназначена для обработки элементов массива с сохранением промежуточного результата. Таким образом, в итоге мы можем получить некий интегральный результат для данного массива. Например, с помощью функции reduce можно вычислить сумму элементов массива: Аргументами функции reduce являются: Начальное значение, которое передается в качестве результата предыдущего действия при обработке первого элемента массива. Теперь посмотрим, как использовать функцию reduce для построения цепочки обещаний по поставленной задаче: В этом примере в качестве результата предыдущего действия выступает обещание actionsChain, после разрешения которого создается новое обещание выполнить действие doSomething, которое в свою очередь возвращается в качестве результата. Так выстраивается цепочка для каждого элемента массива. Promise.resolve() используется в качестве начального обещания actionsChain. В качестве более сложного примера применения обещаний рассмотрим фрагмент кода сервиса генерации уникальных идентификаторов. Суть работы сервиса очень проста: на каждый запрос GET к сервису он вычисляет и возвращает очередной уникальный идентификатор. Однако, учитывая то, что одновременно к сервису может быть направлено несколько конкурирующих запросов, возникает задача выстраивания этих запросов в очередь (цепочку) обещаний для последующего выполнения. Код самого сервера выглядит так: Это стандартный код http-сервера, написанного на node.js. Функция processRequest — обработчик http-запроса. Сам сервис использует для хранения своего состояния хранилище redis. Механизм сохранения состояния в redis не имеет значения для понимания данного примера, поэтому рассматриваться не будет. Но при завершении работы сервера нам необходимо закрыть соединение с redis, что и отражено в коде выше. Важно только следующее: при каждом запросе вызывается функция processRequest. Очередь запросов представляет собой обещание requestQueue. Рассмотрим теперь сам код: Для вычисления самого идентификатора используется функция calcUUID(), которая возвращает обещание вычислить уникальный идентификатор. Код этой функции рассматривать не будем, так как для понимания этого примера он не нужен. При получении запроса к очереди обработки добавляется обещание вычислить очередной идентификатор. Таким образом, несколько конкурирующих запросов выстраиваются в одну цепочку, где вычисление каждого следующего идентификатора начинается после вычисления предыдущего. Таким образом, обещания позволяют контролировать процесс выполнения асинхронных вызовов, делая при этом код читаемым и доступным для понимания. Необходимо только научиться использовать механизмы обещаний и шаблоны программирования с использованием обещаний. Надеюсь, что все было понятно. И каждый нашел в этой статье что-то полезное для себя. Программист