Знакомство с promises — одним из нововведений ES6
27К открытий27К показов
Что такое promise?
Вообще говоря, promises (дословно — “обещания”) — это обёртки для функций обратного вызова (callback). Их можно использовать для упорядочивания синхронных и асинхронных действий.
С помощью promises вы сможете переписать:
… в таком виде:
Может быть непонятно, почему нижний вариант лучше — а лучше он потому, что в нём отсутствует вложенность. Представьте, что наш пример гораздо длиннее. Верхний вариант имел бы дюжину вложенных функций и был бы очень трудночитаем, тогда как нижняя версия представляла бы собой прямолинейную последовательность действий и мало отличалась бы от случая синхронных действий.
Обещание поддерживает 2 метода: then
и catch
. then
принимает продолжение, являющееся функцией обратного вызова, принимающей результат в качестве аргумента и возвращающей новое обещание или другое значение. Аналогично, catch
— это callback, вызываемый при возникновении исключения или другой ошибки.
Полная версия then
в себя оба поведения:
Так, catch
можно определить следующим образом:
Как создать обещание?
Для создания обещания вам не нужно создавать объект с методами then
и catch
. Вместо этого можно использовать конструктор Promise
:
Достаточно вызвать resolve
, когда обещание выполнено, или вызвать reject
, если что-то пошло не так. Также можно сгенерировать исключение. Если вы хотите обернуть значение в обещание, которое будет выполнено немедленно, можно просто написать Promise.resolve(value)
. В обратном случае достаточно написать Promise.reject(error)
.
Таймауты
Функция setTimeout
используется для выполнения кода после определенной задержки. Вот её версия, реализованная с помощью promises:
Вот пример использования:
Вы можете удивиться, что delay
каррирована. Это сделано для того, чтобы её можно было включить в последовательность после другого обещания:
AJAX
AJAX — классический пример использования promises. Если вы используете jQuery, вы увидите, что $.ajax
не вполне совместима с promises, потому что не поддерживает catch
. Но мы с легкостью можем создать обёртку:
Пример:
Или, если вы не пользуетесь jQuery, вы можете создать обёртку для XMLHttpRequest
:
Пример:
Отложенное исполнение
Вопрос: в каком порядке будут выведены строки после исполнения этого кода?
Ответ: A
, C
, затем B
.
Удивительно, правда? then
–переходник отложен, а тот, что передан в конструктор Promise
— нет. Но мы можем воспользоваться таким поведением then
. Часто хочется отложить выполнение некоторого куска кода до завершения асинхронной части. Раньше для этого можно было использовать setTimeout(func, 1)
но теперь для этого можно написать обещание:
Его можно использовать так:
Этот код выведет B
, затем A
. Хотя это и не короче, чем setTimeout(func, 1)
, это разъясняет наши намерения и совместимо с другими обещаниями. Таким образом, мы получаем более структурированный код.
Финальные замечания
Истинная ценность обещаний проявляется, когда весь асинхронный код структурирован с их помощью. Используя компонуемые обещания для таймеров или AJAX-действий, легко избежать вложенности и писать более линейный код.
Я был удивлён несколькими вещами:
- Функции обратного вызова для
then
иcatch
могут возвращать любое значение, но они ведут себя по-другому. Обычно, возвращаемое значение передаётся следующему заthen
выражению в цепочке. Но если это значение — обещание, дальше передаётся значение, возвращаемое обещанием при его выполнении. Это значит, что возвратPromise.resolve(x)
эквивалентен возвратуx
. - Функция, переданная в конструктор
Promise
, выполняется синхронно, но любые продолжения черезthen
илиcatch
будут отложены до следующего цикла событий. Этим объясняется работаdefer
. catch
— это зарезервированное ключевое слово, используемое для обработки исключений в JavaScript, но это также и имя метода, закрепляющего обработчик ошибок в обещании. Это похоже на неудачную коллизию имён. С другой стороны, это облегчает запоминание, так что не всё так плохо!
Первый пункт кажется мне странным из-за следующего мысленного эксперимента: что, если мы действительно хотим передать обещание следующей функции в цепочке? Если мы попытаемся сделать это таким наивным образом, обещание лишится обёртки, и мы не получим желаемый результат. Вы можете спросить: зачем вообще передавать обещание в callback? Ну, может быть, код не знает, какого типа значение он передаёт в функцию. Может быть, значение создаётся где-то ещё в программе, и оно просто должно быть передано. Теперь нужно сперва проверить, если это обещание, и в этом случае обернуть его во что-то ещё для защиты на время передачи. Поэтому я бы предпочёл, чтоб функции обратного вызова всегда должны были возвращать обещание (даже если значение обёрнуто в Promise.resolve(value)
).
Обдумав второй пункт, я пришёл к выводу, что функции обратного вызова должны быть отложенными. И вот почему: допустим, обещание не выполнено. Оно должно быть передано следующему в цепочке обработчику ошибок, или же сгенерировать исключение при отсутствии оного. Но ему сперва придётся дождаться, пока обработчики ошибок будут прикреплены. И как долго ждать? Очевидно: до следующей итерации цикла событий.
Несмотря на эти странности, promises — приятное дополнение к JavaScript. Ад callback’ов был одним из моих самых нелюбимых аспектов JavaScript, но теперь его можно избежать.
P.S. Promise
— это монада! Ну или, как минимум, могло бы ею быть, если бы не первый пункт.
27К открытий27К показов