9 полезных советов по Promise.resolve и Promise.reject


Оглавление (нажмите, чтобы открыть):

9 полезных советов по Promise.resolve и Promise.reject

Группа: Главные администраторы
Сообщений: 14349
Регистрация: 12.10.2007
Из: Twilight Zone
Пользователь №: 1

JavaScript*
На Хабре уже встречались статьи о замечательной технологии Promises, которая в будущем станет частью стандарта ECMAScript 6, однако, в этих статьях не было подробного описания, почему же они так полезны и в чем таки их преимущества. Дабы заполнить этот пробел, я решил написать эту статью.

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

Итак, что же такое Promise?

  • Promise это объект, используемый как заглушка для результата некоего отложенного (и возможно асинхронного) вычисления
  • Способ писать последовательно/параллельно выполняемый асинхронный код как синхронный
  • Часть стандарта ECMAScript 6
  • Конструктор Promise — это имплементация паттерна Revealing Constructor Pattern
  • «Статичные» методы
  • Promise.resolve(val) and Promise.reject(val)
  • Promise.all and Promise.race
  • Концепция “Thenable” object
  • В двух словах — это объект, обладающий методами .then() and .catch()
  1. Fulfilled — вычисление было успешным
  2. Rejected — ошибка при вычислении (любая)
  3. Pending — вычисление еще не завершилось (не fulfilled и не rejected)
  4. Settled — вычисление завершилось (не важно как)

Итак, возьмем кусок синхронного кода

function foo() <
var a = ‘a’;
a = a + ‘b’;
a = a + ‘c’;
return a;
>

И сделаем так, чтобы снаружи он выглядел как Promise:

function foo() <
var a = ‘a’;
a = a + ‘b’;
a = a + ‘c’;
return Promise.resolve(a);
>

Следующий шаг — раздели каждый этап вычисления:

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

Пойдем дальше, заменим один из шагов на «как бы асинхронную функцию, возвращающую Promise»:

Встроенная функциональность допускает возврат в then(cb()) либо ошибки (throw new Error()) либо значения (return a+’c’;) либо следующего Promise.

Представим, что сперва надо выполнить асинхронное действие 1, затем параллельно 2 и 3, и затем 4.

asyncAction1()
.then(function(res1) <
return Promise.all([async2(res1), async3(res1)]);
>).
.then(function(arr) < // an array of values
var res2 = arr[0], res3 = arr[1];
return asyncAction4(res2, res3);
>).
then(…);

Самая замечательная вещь в Promises — это обработка ошибок. Не важно, на каком этапе и в какой глубине вложенности произошла ошибка, будь то reject или просто брошенное исключение, все это можно поймать и обработать, либо же прокинуть дальше.

asyncAction()
.catch(function(rejection) <
// пытаемся понять, можно ли как-то обработать ошибку
if (rejection.code == ‘foo’) return ‘foo’;
// никак нельзя, прокидываем ошибку дальше
throw rejection;
>)
.then(…)
.then(…)
.catch(…);

Здесь нужно сделать замечание, что если, положим

var p1 = new Promise(. ),
p2 = new Promise(. )
p3 = Promise.all([p1, p2]);

Catch будет ловить все, что пошло не так (причем не важно, что и как именно) в p1, p2, p3 и любых вложенных вызовах, что дико удобно. Обратная сторона — если catch() нет, то ошибка будет тихо проглочена. Однако библиотеки типа Q, как правило, имеют возможность задать обработчик непойманных ошибок, где их можно вывести в консоль или сделать что-то еще.

Слово об анти-паттернах

function anAsyncCall() <
var promise = doSomethingAsync(); promise.then(function() <
somethingComplicated();
>);
return promise;
>

Иии легким движением руки мы потеряли второй Promise. Дело в том, что каждый вызов .then() или .catch() создает новый Promise, поэтому если создали новый, а вернули старый, то новый повиснет где-то в воздухе и никто не узнает, каков результат вычисления. Как бороться — просто вернуть новый Promise:

function delay(ms) <
return new Promise(function(resolve) <
setTimeout(resolve, ms);
>
>;

Поскольку Promise может быть settled только один раз (остальное игнорируется), то можно написать что-то вроде

function timeout(promise, ms) <
return new Promise(function (resolve, reject) <
promise.then(resolve);
setTimeout(function () <
reject(new Error(‘Timeout’));
>, ms);
>);
>

Кто первый встал — того и тапки.

Немного улучшенный таймаут

Чуть более очевидный пример таймаута через «статичную» функцию Promise.race().

Promise.race([
asynchronousAction(),
delay(5000).then(function () <
throw new Error(‘Timed out’);
>)
])
.then(function (text) < . >)
.catch(function (reason) < . >);

Важное замечание об асинхронности

  1. Библиотека контролирует процесс выполнения, соответственно она заведует, как доставляется результат — синхронно или асинхронно
  2. В то же время, спецификация Promises/A+ требует, чтобы всегда использовался последний режим — асинхронный
  3. Таким образом, мы всегда можем полагаться на немедленное выполнение кода promise.then().catch() и т.д., и не заботиться, что какой-то их коллбеков съест все процессорное время (с оговорками, само собой)

Почему Promises лучше callbacks

  • Они часть стандарта — умные люди думали и разрабатывали всякое для нашего удобства
  • «Практически» не сказываются на производительности (http://thanpol.as/javascript/promises-a-pe. uld-be-aware-of)
  • Попробуйте разрулить нетривиальный поток выполнения на callbacks, желательно с обработкой ошибок, если вы не напишете свою имплементацию Promises, код, скорее всего, будет невозможно читать и понимать
  • Все Promises/A+ совместимые библиотеки могут принимать объекты друг друга (Angular прекрасно работает с объектами Q/Native Promise/RSVP и т.д.)
  • А еще Promise — лучше, чем Deferred, потому что последний — это две концепции в одной, что безусловно плохо, а кроме этого, паттерн Revealing Constructor почти что гарантирует, что Promise будел settled только в рамках этого конструктора (ну или вы сам себе злобный Буратино)
  1. Не подходит для повторяющихся событий (правда, Promises не для того и писались)
  2. Не подходит для streams (аналогично)
  3. Текущая реализация в браузерах не позволяет следит за progress (собираются тоже включить в стандарт)

Замечание о jQuery

Реализация «Thennable» в jQuery немного отличается от стандартной — в стандарте аргумент в callback ровно один, в jQuery кол-во аргументов больше, что не мешает использовать объект, полученный из jQuery в нативной реализации. Как правило, больше, чем первый аргумент, ничего от jQuery и не нужно. Плюс существует конструкция:

Отменяемые промисы :: Хранитель заметок

Отменяемые промисы

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

Дополнительный метод .cancel()

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

Отсутствие очистки после отмены

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


speculation

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

По аналогии с конструктором Promise , первым аргументом передается функция «исполнитель», а вторым аргументом передается промис для отмены.

Функция-исполнитель принимает 3 аргумента и реализует логику разрешения, отклонения и отмены. Вот почему у самого промиса не нужен дополнительный метод .cancel() , и при этом вся логика, связанная с отменой, реализована внутри функции, которая создаёт этот промис.

Отменённый промис получает статус rejected.

AbortController и AbortSignal

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

Значение signal.aborted меняется только из контроллера. Одновременно с этим испускается событие abort . В спецификации дан пример, как можно применять AbortController и AbortSignal для своих API.

Отменённый промис так же получает статус rejected, а значение устанавливается в new DOMException(‘Aborted’, ‘AbortError’) .

JavaScript метод Promise.resolve()

Определение и применение

JavaScript метод .resolve() объекта Promise возвращает объект Promise , который был успешно выполнен с заданным значением (изменяет состояние объекта Promise на fulfilled — успешное выполнение). Метод .resolve() , как правило, используется для преобразования какого-то значения в объект Promise .

Поддержка браузерами

Метод Chrome

Firefox Opera Safari IExplorer Edge
.resolve() 32.0 29.0 19.0 8.0 Нет Да

JavaScript синтаксис:

Спецификация

Значения параметров

Параметр Описание
value Необязательные аргументы (параметры), которые будут переданы для функций обратного вызова. В качестве аргумента может содержать объект Promise , или thenable объект подобный обещанию. Под thenable объектом стоит понимать любой объект (или функция), обладающий методом then(). Если значение ­распознается как обещание, или объект thenable, то его состояние просто заимствуется.

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

Простое значение в качестве аргумента

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

Кроме того, с использованием метода then() добавили обработчики, вызываемые когда объект Promise имеет состояние fulfilled (успешное выполнение), или rejected (выполнение отклонено). В нашем случае всегда будет срабатывать обработчик для успешного выполнения. Обратите внимание, что мы используем метод forEach() объекта Array для возможности вывести все значения из массива.

Объект Promise в качестве аргумента

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

Thenable объект в качестве аргумента

В этом примере мы в качестве аргумента метода .resolve() передаем thenable объект подобный обещанию. С использованием метода then() добавили обработчики, вызываемые когда объект Promise имеет состояние fulfilled (успешное выполнение), или rejected (выполнение отклонено). В нашем случае будет срабатывать обработчик для успешного выполнения.

В этом примере мы создаем переменную, содержащую thenable объект подобный обещанию. Метод объекта генерирует исключение и при этом метод .resolve() , который содержит этот метод никогда не будет вызван, так как до этого было сгенерировано исключение. Если исключение было бы размещено после вызова метода .resolve() , то оно не было бы вызвано.

Кроме того, мы с помощью метода then() добавили обработчики, вызываемые когда объект Promise имеет состояние fulfilled (успешное выполнение), или rejected (выполнение отклонено). В нашем случае будет срабатывать обработчик при отклоненном выполнении.

Javascript Promise. Как прервать длинную цепочку then?

Если цепочка промисов:

Нужно иметь возможность прервать ее и, например, запустить функцию, создающую промис, снова, либо любую дургую, предшествующую ей, не суть. reject пока не использую, при ошибке делаю resolve(0). Да и reject не решает проблему:

Пока вижу только такой выход: при ошибке делать resolve(0) и на каждом этапе делать проверку, проталкивая ошибку до конца:

Но это лишний код. Как сделать так, чтобы следующий .then просто не сработал?

Frontier who watches the watchmen?

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

Нет, не промисами как таковыми. Спецификация A+ превосходна.

А с тем, что я вижу множество программистов, не справляющихся с API PouchDB и другими API, построенных на промисах. Их беда в том, что Многие из нас используют промисы, не до конца их понимая.

В это сложно поверить, но посмотрите на задачку, которую я недавно запостил в твиттер:

В чём разница между этими промисами?

Если вы знаете ответ, то поздравляю: вы мастер промисов. Можете дальше не читать эту статью.

Для остальных 99.99% из вас, вы не одиноки. Никто из ответивших на мой твит не смог решить эту задачу, а ответ на #3 меня и самого удивил. Да, несмотря на то, что я сам написал этот тест!

Ответы приведены в конце, но сначала, я бы хотел исследовать, почему промисы вообще такие каверзные, и почему столь многие из нас — и новички, и эксперты — попадаются на эту удочку. Я также хочу предложить то, что мне кажется моментом прозрения, один хитрый трюк, чтобы влёгкую понять промисы. И да, я правда считаю, что они не такие уж и сложные!

Итак, промисы?

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

Промисы действительно решают эту проблему, но они не только про отступы. Как нам рассказали в великолепном выступлении «Избавление от ада коллбэков», настоящая беда с коллбэками в том, что они отбирают у нас такие вещи, как return и throw . Вместо этого, весь поток нашей программы основан на сайд-эффектах: одна функция невзначай вызывает другую.

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

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

Ошибки новичков

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

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

Лирическое отступление : «промисы» означают для разных людей много разных вещей, но в этой статье я буду говорить о них, подразумевая официальную спецификацию, реализованную в современных браузерах как window.Promise . Не во всех браузерах есть window.Promise , так что для хорошего полифилла посмотрите на библиотеку с остроумным названием Lie, чуть ли не самую маленькую из соответствующих спецификации.

Ошибка новичка #1: пирамида зла из промисов


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

Самое распространенное плохое решение вот это:

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

Этот стиль гораздо лучше:

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

Ошибка новичка #2: WTF, как мне сделать forEach() по промисам?

Здесь у большинства людей ломается понимание промисов. Как только они тянутся к знакомому циклу forEach() (или циклу for , или while ), у них нет идей, как заставить их работать с промисами. Так что они пишут что-то вроде:

Что не так с этим кодом? То, что первая функция вернёт undefined , а это значит, что вторая функция не будет дожидаться, пока db.remove() будет вызвана на всех документах. На самом деле, она вообще ничего не будет ждать и может быть выполнена с любым количеством удаленных документов!

И это особенно коварный баг, потому что вы можете не заметить, что что-то не так, если PouchDB удалит документы быстрее, чем обновляется ваш интерфейс. Баг может всплыть только в странных race conditions или в определенных браузерах, и тогда его будет практически невозможно отдебажить.

Что только что произошло? Promise.all() берёт массив промисов на вход, а затем возвращает другой промис, который исполнится тоько тогда, когда зарезолвится каждый из этих промисов. Это асинхронный эквивалент цикла for .

Promise.all() также передаёт массив результатов следующей функции, что зачастую пригождается, например, если вы хотите сделать get() сразу нескольких вещей из PouchDB. Промис all() также будет отклонен, если любой из под-промисов будет отклонен, что ещё полезнее.

Ошибка новичка #3: забыть добавить .catch()

Это другая распространенная ошибка. Блаженно верующие в то, что их промисы никогда не отклоняются, многие разработчики забывают добавить .catch() где-либо в своем коде. К несчастью, это приведет к тому, что любая брошенная ошибка будет проглочена, и вы даже не увидите её в консоли. Это реально сложно дебажить.

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

Даже если вы не ждёте ошибки, мудрым решением будет всегда добавить catch() . Ваша жизнь станет проще, особенно если ваши предположения окажутся неверными.

Ошибка новичка #4: использовать deferred

Эту ошибку я вижу всё время, и я не хочу даже повторять это здесь, из-за страха, что подобно Битлджусу, просто назвать имя — значит вызвать ещё больше его же.

Вкратце, у промисов долгая и бурная история, и на то, чтобы сделать всё правильно, у сообщества джаваскриптеров ушло много времени. В ранние дни, jQuery и Angular использовали повсюду паттерн deferred, который сейчас был заменен спецификацией ES6 Promise, которую реализуют «хорошие» библиотеки, такие как Q, When, RSVP, Bluebird, Lie и другие.

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

Во-первых, многие библиотеки дают вам способ «импортировать» промисы от других библиотек. Например, модуль $q из Ангуляра позволяет вам обернуть не- $q шные промисы через $q.when() . Так что пользователи Ангуляра могут обернуть промис от PouchDB таким образом:

Другой способ — использовать паттерн раскрывающегося конструктора, что удобно для оборачивания API без промисов. Например, чтобы обернуть основанное на коллбэках API вроде fs.readFile() из node.js, нужно просто:

Готово! Мы победили страшный def… Ха, поймал себя. 🙂

Ошибка новичка #5: использовать сайд-эффекты вместо возврата значения

Что не так с этим кодом?

Хорошо, пришло время рассказать всё, что вам нужно знать об промисах.

Серьёзно, это один странный трюк, который, когда вы его поймёте, предотвратит все ошибки, о которых я говорил выше. Готовы?

Я говорил, что магия промисов в том, что они возвращают нам обратно драгоценные return и throw . Но как это выглядит на практике?

Каждый промис даёт вам метод then() (или `catch(), который просто сахар для then(null, . ) ). Вот мы внутри функции then() :

Что можно здесь сделать? Три вещи:

  1. вернуть другой промис через return
  2. вернуть синхронное значение через return (или undefined )
  3. бросить синхронную ошибку через throw

Это всё. Как только вы поймёте этот трюк, вы поймёте промисы. Поэтому давайте пройдёмся по каждому пункту по отдельности.

1. Вернуть другой промис

Это распространненый паттерн, который можно найти во многих текстах про промисы, как в примере про «композицию промисов» выше.

Отметьте, что я возвращаю второй промис — этот return очень важен. Если бы его не было, то getUserAccountById() был бы сайд-эффектом и следующая функция получила бы undefined вместо userAccount .

2. Вернуть синхронное значение (или undefined )

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

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

Неудобная неприятность в том, что функции без return в Javascript, строго говоря, возвращают undefined , а это значит, что легко случайно привнести сайд-эффекты тогда, когда вы хотели вернуть что-то другое.

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

3. Бросить синхронную ошибку

Кстати, о throw , здесь промисы становятся ещё лучше. Скажем, мы хотим бросить синхронную ошибку, если пользователь вышел из аккаунта. Это легко:

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

Это особенно полезно, потому что может помогает находить ошибки в процессе разработчки. Например, если где-то внутри then() мы делаем JSON.parse() , который кидает синхронную ошибку, если JSON не корректен. С коллбэками, ошибка была бы проглочена, но с промисами мы можем просто обработать её внутри catch() .

Продвинутые ошибки

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

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

Продвинутая ошибка #1: не знать о Promise.resolve()

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

То вы можете выразить это лаконичнее, используя Promise.resolve() :

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

Просто запомните: код, который может кидать синхронные ошибки, — хороший кандидат для ошибок, которые почти невозможно отдебажить из-за того, что они проглочены где-нибудь по дороге. Но если оборачивать всё в Promise.resolve() , то всегда можно будет поймать их в catch() позже.

Аналогично, есть Promise.reject() , который возвращает промис, который немедленно отклоняется:


Продвинутая ошибка #2: catch() немного не then(null, . )

Выше я говорил, что catch() — просто синтаксический сахар. Так что эти два фрагмента эквивалентны:

Но это не значит, что эти два фрагмента одинаковы:

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

Когда вы используете формат then(resolveHandler, rejectHandler) , rejectHandler не поймает ошибку, если её кинет resolveHandler .

Поэтому я просто не использую второй аргумент у then() и всегда предпочитаю catch() . Я делаю исключение только для случаев, когда я пишу асинхронные тесты для Mocha, где я могу написать тест на то, что ошибка действительно кидается:

И раз мы уж тут, Mocha и Chai образуют хорошую комбинацию для тестирования API с промисами. В проекте pouchdb-plugin-seed есть несколько шаблонных тестов, чтобы начать было легче.

Продвинутая ошибка #3: промисы или фабрики промисов

Скажем, вы хотите исполнить несколько промисов один за другим последовательно. То есть, вам нужно что-то вроде Promise.all() , но которое не выполняет промисы параллельно.

Наивный код будет выглядеть как-то так:

К несчастью, он не будет работать так, как вы предполагали. Промисы, которые вы передадите в executeSequentially() всё ещё будут исполняться параллельно.

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

Я знаю, что вы сейчас подумали: «Кто пустил сюда джава-программиста, и почему он говорит о фабриках?». Но фабрика промисов это просто функция, которая возвращает промис:

Почему это работает? Потому что фабрика не создаёт промис до тех пор, пока её не попросят. Работает так же, как и функция then — в сущности, это одно и то же!

Если вы посмотрите на функцию executeSequentially() , а затем представите, что myPromiseFactory подставляется внутрь result.then(. ) , то, надеюсь, у вас зажгётся лампочка над головой. В этот момент вы достигли просветления промисов.

Продвинутая ошибка #4: ладно, а что если я хочу результат двух промисов?

Часто один промис зависит от другого, но нам нужен результат обоих. Например:

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

Это работает, но мне кажется немного неуклюжим. Я рекомендую следующее: отбросьте ваши предубеждения и поддержите пирамиду:

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

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

Промисы как раз про это.

Продвинутая ошибка #5: проваливание промисов

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

Как вы думаете, что выведет этот код?

Если вы думаете, что он выведет bar, то вы ошиблись. Он выведет foo!

Так случается потому, что когда вы передаёте в then() не функцию (а, например, промис), то это интерпретируется как then(null) , что заставляет проваливаться дальше результат предыдущего промиса. Можете сами проверить:

Добавьте сколько вам угодно then(null) , оно всё равно выведет foo.

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

Это выведет bar, как мы и ожидали.

Так что запомните: всегда передавайте функцию в then() !

Решение задачи

Теперь, когда мы узнали всё про промисы (или почти всё!), мы сможем решить задачу, которую я поставил в начале этого поста.

Вот ответы для каждого из примеров в графическом формате, чтобы было проще представить:

Пример #1

Пример #2

Пример #3

Пример #4

Если эти ответы всё ещё кажутся вам странными, то я рекомендую вам перечитать статью ещё раз, или определить методы doSomething() и doSomethingElse() и попробовать их в браузере.

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

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

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

Если вы всё ещё мне не верите, вот доказательство: рефакторинг модуля map/reduce в PouchDB, чтобы перейти с коллбэков на промисы. Результат: 290 вставок, 555 удалений.

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

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

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

В идеале, вы не должны быть обязанными выучить кучу неясных правил и новых API чтобы делать то, что в синхронном мире замечательно делается при помощи return , catch , throw и циклов for . Не должно быть двух параллельных систем, которые надо всё время держать в голове.

Ждём async / await

Это то, что я показал в статье «Приручаем асинхронного зверя в ES7, где я исследовал ключевые слова async / await из ES7 и как они глубже интегрируют промисы в язык. Вместо того, чтобы писать псевдо-синхронный код (с фейковым catch() , который похож на catch , но не слишком), в ES7 мы сможем использовать настоящие try / catch / return , точь-в-точь как мы научились на первом курсе.

Это большой плюс Javascript как языку. В конце концов, анти-паттерны с промисами всё равно будут всплывать, если наши инструменты не будут сообщать нам, что мы допускаем ошибку.

Возьмём пример из истории Javascript, я думаю, что честным будет сказать, что JSLint и JSHint оказали большую услугу сообществу, чем книга Javascript: The Good Parts, хотя в них содержится, в общем-то, одна и та же информация. Разница в том, что в одном случае тебе говорят какую именно ты допустил ошибку и где, а в другом это просто книга, где ты пытаешься понять ошибки других.

Прелесть async / await из ES7 в том, что, по большей части, ошибки будут в виде неверного синтаксиса или ошибки компиляции, нежели тонких багов во время исполнения. Но пока эти времена не пришли, всё-таки хорошо понять на что способны промисы, и как их правильно использовать в ES5 и ES6.


Я осознаю, что этот пост, как и Javascript: The Good Parts, будет иметь ограниченное влияние, но надеюсь, что на него можно будет кинуть ссылку людям, когда вы заметите у них эти самые ошибки. Потому что многим из нас стоит признать: «У меня проблема с промисами»

9 полезных советов по Promise.resolve и Promise.reject

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

У Promise есть два (интересующих нас сейчас) метода — .then() и .catch() , в которые передаются функции-обработчики для состояний «выполнено» (fulfilled) и «отказ» (rejected) соответственно. Эти методы можно объединять в цепочки: prom.then().then().catch().then().catch( ).catch().… . Вопрос: в каком порядке вызываются эти обработчики, что они получают, и от чего всё это вообще зависит?

Для начала посмотрим на конструкцию prom.then().then() (здесь prom — это некоторый Promise). Что такое тут prom.then() ? Очевидно, это Promise, раз у него есть методы .then() и .catch() . Каково значение этого Promise? «Естественным» может показаться предположение, что это тот же самый prom передаётся по цепочке, но на самом деле это не так (и хорошо, что не так).

Каждый обработчик .then() и .catch() возвращает какое-то значение. Даже если функция не возвращает значение явно, то результатом её всё равно является значение undefined . Далее действуют правила:

  • Если функция возвращает Promise, то именно он и становится новым Promise-ом в цепочке;
  • Если функция возвращает любое другое значение, то оно оборачивается в Promise.resolve(…) и становится Promise-ом в выполненном состоянии;
  • Наконец, если в функции произошло исключение, то значение этого исключения оборачивается в Promise.reject(…) и становится Promise-ом в состоянии отказа.

Самое полезное применение этих правил — возможность обработки данных в цепочке. Например, так:

Но вернёмся к произвольным цепочкам. У нас есть Promise, в зависимости от его состояния, он вызывает .then() или .catch() , ближайшие к нему по цепочке. Вызванный обработчик возвращает новый Promise, который снова вызывает .then() или .catch() , ближайшие по цепочке. И так далее.

Приведём пример (писать буду в ES6-синтаксисе, он проще):

Что мы получим в консоли? Вот что:

Почему так? У нас есть Promise в состоянии «отказ», значит, он вызовет ближайший обработчик .catch() в цепочке, это CATCH1. Этот обработчик не возвращает ничего (т. е. возвращает undefined ), значит, на выходе из него мы получаем выполненный Promise со значением undefined . Выполненый Promise вызывает ближайший к нему .then() , это THEN2.

Результат (посчитайте сами, почему именно так):

Знакомство с promises — одним из нововведений ES6

Что такое 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 — классический пример использования promises. Если вы используете jQuery, вы увидите, что $.ajax не вполне совместима с promises, потому что не поддерживает catch . Но мы с легкостью можем создать обёртку:

Или, если вы не пользуетесь jQuery, вы можете создать обёртку для XMLHttpRequest :

Отложенное исполнение

Вопрос: в каком порядке будут выведены строки после исполнения этого кода?

Ответ: A , C , затем B .

14 ноября в 18:30, Витебск, беcплатно

Удивительно, правда? then –переходник отложен, а тот, что передан в конструктор Promise — нет. Но мы можем воспользоваться таким поведением then . Часто хочется отложить выполнение некоторого куска кода до завершения асинхронной части. Раньше для этого можно было использовать setTimeout(func, 1) но теперь для этого можно написать обещание:

Его можно использовать так:

Этот код выведет B , затем A . Хотя это и не короче, чем setTimeout(func, 1) , это разъясняет наши намерения и совместимо с другими обещаниями. Таким образом, мы получаем более структурированный код.

Финальные замечания

Истинная ценность обещаний проявляется, когда весь асинхронный код структурирован с их помощью. Используя компонуемые обещания для таймеров или AJAX-действий, легко избежать вложенности и писать более линейный код.

Я был удивлён несколькими вещами:

  1. Функции обратного вызова для then и catch могут возвращать любое значение, но они ведут себя по-другому. Обычно, возвращаемое значение передаётся следующему за then выражению в цепочке. Но если это значение — обещание, дальше передаётся значение, возвращаемое обещанием при его выполнении. Это значит, что возврат Promise.resolve(x) эквивалентен возврату x .
  2. Функция, переданная в конструктор Promise , выполняется синхронно, но любые продолжения через then или catch будут отложены до следующего цикла событий. Этим объясняется работа defer .
  3. catch — это зарезервированное ключевое слово, используемое для обработки исключений в JavaScript, но это также и имя метода, закрепляющего обработчик ошибок в обещании. Это похоже на неудачную коллизию имён. С другой стороны, это облегчает запоминание, так что не всё так плохо!

Первый пункт кажется мне странным из-за следующего мысленного эксперимента: что, если мы действительно хотим передать обещание следующей функции в цепочке? Если мы попытаемся сделать это таким наивным образом, обещание лишится обёртки, и мы не получим желаемый результат. Вы можете спросить: зачем вообще передавать обещание в callback? Ну, может быть, код не знает, какого типа значение он передаёт в функцию. Может быть, значение создаётся где-то ещё в программе, и оно просто должно быть передано. Теперь нужно сперва проверить, если это обещание, и в этом случае обернуть его во что-то ещё для защиты на время передачи. Поэтому я бы предпочёл, чтоб функции обратного вызова всегда должны были возвращать обещание (даже если значение обёрнуто в Promise.resolve(value) ).

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

Несмотря на эти странности, promises — приятное дополнение к JavaScript. Ад callback’ов был одним из моих самых нелюбимых аспектов JavaScript, но теперь его можно избежать.

Промисы в JavaScript для чайников

Промисы в JavaScript на самом деле совсем не сложные. Тем не менее, многие люди находят затруднительным их понимание с первых моментов изучения. Поэтому, снизу всё будет описано способом для начинающих, чтобы читающий понял материал полностью и объемно. Помните, что промисы лежат в основе async/await и их понимание, можно сказать, что обязательно для работы с асинхронным JavaScript.

Понимание промисов

Итак, вкратце про промисы: “ Представьте, что вы ребенок. Ваша мама обещает вам, что вы получите новый телефон на следующей неделе.”

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

Это и есть промис. От английского promise — обещать. Небольшое уточнение, пожалуйста, не усложняйте понимание другим с произношением, так как во всём русскоязычном мире принято говорить “промис” в случае с JavaScript.

Итак, у промиса есть 3 состояния. Это:


1. Промис в состоянии ожидания ( pending ). Когда вы не знаете, получите ли вы мобильный телефон к следующей неделе или нет.

2. Промис решен ( resolved ). Вам реально купят новый телефон.

3. Промис отклонен ( rejected ). Вы не получили новый мобильный телефон, так как всё-таки, мама была не в настроении.

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

Давайте переведем все это в JavaScript.

Код выше довольно выразителен и говорит сам за себя.

1. У нас есть булин isMomHappy , чтобы определить в каком расположении духа мама.

2. У нас есть промис willIGetNewPhone . Этот промис может быть как в состоянии resolved , то есть, если вы получаете мобильный телефон, а также может быть в состоянии rejected , то есть если ваша мама не в настроении и вы не получаете мобильный телефон.

3. Тут у нас стандартный синтаксис для определения нового промиса, как в MDN документации. То есть синтаксис промиса выглядит таким образом.

4. Когда вам нужно это запомнить, когда результат успешен, вызывайте resolve (ваше значение при успехе), если результат не успешен, вызывайте reject (ваше значение при неудаче, соответственно). В нашем случае мама в настроении и мы получим телефон. Следовательно, мы вызываем resolve функцию с переменной phone . Если ваша мама не в настроении, мы вызовем функцию reject с reason , то есть reject ( reason ).

Этот блог бесплатный, в нём нет рекламы и ограничений paywall.
Вы можете его поддержать через Яндекс.Деньги. Спасибо.

Применяем промисы

Теперь у нас есть промис, давайте применим его, ну или употребим, как хотите.

1. Мы вызываем функцию в askMom . В этой функции, мы применим наш промис willIGetNewPhone .

2. Нам надо сделать одно действие, чтобы промис был решен или отклонен, тут мы будем использовать .then и .catch .

3. В нашем примере, у нас function(fulfilled) <…>в .then . Какое значение у fulfilled ? fulfilled значение это точное значение в вашем промисе resolve (значение при успехе). Следовательно, это будет phone .

4. У нас есть function(error) <…>в .catch . Какое значение будет у error ? Как вы могли предположить, error значение именно то, которое вы указали в промисе reject (значение при неудаче). Следовательно, в этом случае это будет reason .

Давайте запустим этот пример и увидим результат!

Цепочки промисов

Да, в промисах есть цепочки.

Давайте представим, что вы ребенок и обещали своему другу, что покажете ему новый телефон, когда вам его купят. Это будет ещё один промис.

В этом примере вы уже наверное поняли, что мы не вызывали reject . Так как, в принципе, это опционально.

Мы вообще можем сократить этот пример, используя promise.resolve .

А теперь давайте свяжем наши промисы. Вы — ребенок и можете запустить showOff промис только после промиса willIGetNewPhone .

Вот так легко связывать промисы.

Промисы и асинхронность

Промисы асинхронны. Давайте выведем сообщение перед и после вызовом промиса.

Какова последовательность ожидаемого вывода? Возможно вы ожидали.

Но на самом деле вывод будет таким:

Почему? Потому что жизнь (или JS) никого не ждёт.

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

Промисы в ES5, ES6/2015, ES7/Next

ES 5 — поддерживают почти все браузеры. Демо код работает в ES5 среде (Все основные браузеры + NodeJS), если бы вы подключили библиотеку промисов Bluebird. Почему так? Потому что ES5 не поддерживает промисы из коробки. Другая знаменитая библиотека промисов это Q, от Криса Коваля.

ES6 / ES2015 — демо код сработает прямо из коробки, так как ES6 поддерживает промисы естественным путём. Более того, с ES6 функциями, мы можем ещё круче упростить код с помощью => и использовать const и let .

Обратите внимание, что все var заменены на const . Все function(resolve, reject) были упрощены на (resolve, reject) => .

ES7 — Async/await делают синтаксис визуально лучше. ES7 представил async и await синтаксис. Это делает асинхронный синтаксис визуально лучше и проще для понимания, без .then и .catch . Перепишем свой пример с ES7 синтаксисом.

1. Всегда, когда вам нужно возвратить промис в функцию, вы ставите спереди async к этой функции. Для примера, async function showOff(phone)

2. Всякий раз, когда вам надо вызвать промис, вам надо вставить await . Для примера, let phone = await willIGetNewPhone; и let message = await showOff(phone);

3. Используйте try < … >catch(error) < … >, чтобы словить ошибку промиса, отклоненную промисом.

Почему промисы и когда их использовать?

Зачем они нам нужны? Как выглядел мир до промисов? Перед ответом на эти вопросы, давайте вернемся к основам.

Нормальная функция против асинхронной.

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

Нормальная функция для сложения чисел.

Асинхронная функция для сложения двух чисел:

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

Или таким способом вы вообще не можете знать — получите ли вы результат, потому что сервер может просто упасть, тормознуть с ответом и т. п. Вам не нужно, чтобы весь процесс был заблокирован, пока вы ждете результат.

Вызов API, скачивание файлов, чтение файлов — всё это те обычные async операции, которые вы можете выполнять.

Мир до промисов — колбэки.

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

Синтаксис ок, зачем нам тогда промисы?

Что если вы захотите сделать последующее асинхронное действие?


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

Как это выглядит с колбэками?

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

Побег из колбэк ада

Промисы приходят на помощь. Давайте посмотрим на тот же код, но по версии промисов.

С промисами, мы выравниваем колбэк с .then . В этом случае, это выглядит чище, так как нет вложенных колбэков. Конечно же с ES7 async синтаксисом, мы можем даже улучшить этот пример, но это уже на ваше усмотрение.

Новичок на районе: Observables

Перед тем как закончить с промисами, есть кое-что, что пришло для того, чтобы облегчить работу с асинхронными данными — это Observables.

Observables — это ленивые потоки событий, которые могут выдать ноль или больше событий, а могут и вообще не закончиться.

Некоторые ключевые различия между промисами и observables:

Давайте посмотрим на тоже самое демо, только написанное с помощью observables . В этом примере, я использую RxJS.

Observable.fromPromise конвертит промис в observable поток

.do и .flatMap среди некоторых операторов доступных для Observables

Потоки ленивы. Наш addAsync запускается, когда мы .subscribe на него.

Observables могут делать много забавных вещей довольно легко. Для примера, delay добавляет функцию за 3 секунды с всего-лишь одной строкой кода или пробовать заново, так что вы можете делать запрос определенное количество раз.

Заключение

Узнайте про колбэки и промисы. Поймите их и используйте их. Не беспокойтесь о Observables, пока что. Все трое могут повлиять процесс разработки, но всё это будет зависеть от ситуации.

Как работает JavaScript: часть третья, окончание

Это окончание перевода статьи «How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await». В начале мы разобрались с тем, как вообще работает асинхронный код в JS, как работает петля событий и коллбеки. Пришло время поговорить о том, чем плохи коллбеки и какие есть альтернативы.

Вложенные коллбеки

Посмотрите на код:

Тут у нас цепочка из трех функций, вложенных друг в друга, и каждая представляет один шаг асинхронной серии. Код такого типа часто называется «callback hell». Но это понятие и проблема глубже, чем просто вложение и выравнивание кода.

Сперва мы ждем события «click», затем ждем окончания таймера, и наконец ожидаем ответа AJAX, и повторяем все снова. На первый взгляд, код можно разбить на шаги так:

Итак, такой последовательный способ выражения вашего асинхронного кода кажется намного более естественным, не так ли? Должен быть такой вариант, не так ли?

Промисы

Взгляните на код:

Это очень понятный код: он складывает значения x и y, и печатает результат в консоль. Однако что будет, если значение x или y не задано и будет определено позже? Скажем, нам нужно получить значения с сервера, прежде чем мы сможем их использовать в выражении. Представим, что у нас есть функции loadX и loadY, загружающие значения с сервера. И представим, что у нас есть функция, складывающая значения только тогда, когда оба они доступны.

Это может выглядеть примерно так (немного уродливо):

Обратите внимание вот на что: в этом примере мы определяем x и y как будущие значения, и вызываем операцию sum, которая (внешне) не заботится о том, доступны или нет значения в данный момент.

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

Ценность промисов

Давайте примерно прикинем, как мы можем переписать пример с x + y через промисы:

Здесь мы имеем два уровня промисов.

fetchX и fetchY вызываются напрямую, и значения, которые они возвращают (промисы) передаются в sum(). Нижележащие значения, которые эти промисы представляют, могут быть доступны сейчас или позже, но каждый промис приводит свое поведение к общему знаменателю. Мы рассуждаем о значениях x и y независимо от времени. Это будущие значения, точка.

Второй уровень промисов создается функцией sum() через Promise.all([ . ]), результат которого мы ожидаем через this(). Когда операция sum() закончит работу, мы сможем вывести наше будущее значение в консоль. Логика ожидания будущих значений x и y скрыта внутри sum().

Примечания: Внутри sum() вызов Promise.all([ … ]) создает промис, который ждет, пока promiseX и promiseY вернут значения. Вызов .then() создает отдельный промис, который вернет результат values[0] + values[1] сразу, как только он будет доступен. Таким образом, вызов then() после sum() на самом деле работает с этим вторым промисом, а не с первым, созданным Promise.all([ . ]). Кроме того, этот then() тоже возвращает промис, и мы выбираем, как его использовать. Эта система с цепочками промисов будет объяснена детально чуть ниже.

Для промисов вызов then() на самом деле выполняет две функции: первая — заполнение, как было показано выше, а вторая — отклонение:

Если что-то пошло не так при получении x или y, или что-то еще сломалось в процессе, промис, возвращаемый sum(), будет отклонен, и второй обработчик ошибки, переданный в then, получит отклоненное значение промиса.

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

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

Это действительно полезно, когда вы сцепляете промисы:

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

Обещать или не обещать?

Прим. переводчика: я не люблю делать сноски, указывающие на игру слов, но в оригинальном заголовке игра слов: «To Promise or not to Promise?». Однако я решил не переводить promise как обещание, потому что это уже устоявшийся термин, калькированный с английского.

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

Мы знаем, что промисы создаются как new Promise(…), и вы можете подумать, что проверки p instanceof Promise будет достаточно. Ну, не совсем.

В-основном потому что вы можете получить промис из другого окна (например, iframe), и эта проверка не сработает.

Более того, библиотека или фреймворк могут иметь свою реализацию промисов, несовместимую с нативной реализацией ES6. Фактически, вы можете отлично использовать промисы с библиотеками в старых браузерах, не поддерживающих промисы в принципе.

Поглощение исключений

Если на любом этапе создания или ожидания промиса появляется ошибка JavaScript, например TypeError или ReferenceError, выбрасывается исключение, а промис отклоняется. Например:

Но что произойдет, если промис уже вернул значение, и в коллбеке then() происходит исключение? Несмотря на то, что он не будет потерян, процесс его обработки может немного удивить. Пока вы не заглянете глубже:

Может показаться, что исключение из foo.bar() действительно поглощено. На самом деле нет. Немного глубже произошло событие, которое мы не слушаем. Вызов p.then(…) возвращает новый промис, который будет отклонен с исключением TypeError.

Обработка неперехваченных исключений


Есть другие подходы, которые могут показаться удобнее.

Частое предложение — добавить промисам метод done(), который помечает цепь промисов как законченную. done() не возвращает новый промис, так что коллбеки, переданные в done(), не связаны, чтобы не передавать исключения в несуществующую цепочку.

Проблема решается, когда вы явно ожидаете неперехваченные исключения: каждое исключение внутри обработчика done() будет выброшено как глобальное исключение (обычно в консоль разработчика):

Что будет в ES8? Async/await

JavaScript ES8 представляет async/await, что сделает работу с промисами проще. Мы пробежимся по возможностям, предоставляемым async/await, и тому, как можно их использовать.

Так что давайте посмотрим, как это работает.

Вы определяете асинхронную функцию, используя async. Такие функции возвращают объект AsyncFunction. Он представляет асинхронную функцию, выполняющую код внутри себя.

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

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

Вы можете думать, что промисы в JavaScript — аналог Future в Java или Task в C#. Цель async/await — упростить использование промисов. Давайте посмотрим пример:

Аналогично, функции, выбрасывающие исключение эквивалентны функциям, возвращающим отклоненные промисы:

await используется только внутри async функции, и позволяет вам синхронно ожидать результата промиса. Если вы используете промисы снаружи async, вам все еще понадобится коллбек then:

Вы так же можете определить асинхронную функцию как асинхронное выражение. Это похоже по механизму действия и синтаксису на функцию. Главное отличие между асинхронной функцией и выражением состоит в том, что возможно определять анонимные асинхронные функции. Асинхронное выражение может быть использовано как IIFE (Immediately Invoked Function Expression), которое будет выполнено сразу после определения.

Вот как это выглядит:

Но что важнее всего, async/await поддерживается всеми современными браузерами:

Кроме того, поддерживать совместимость помогают транспилеры вроде Babel и TypeScript.

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

5 советов по написанию поддерживаемого и крепкого аcинхронного кода

Чистый код
Использование async/await позволяет вам писать меньше кода. Каждый раз, используя async/await, вы пропускаете некоторые ненужные шаги: .then, создание анонимной функции для обработки ответа, именование ответа из коллбека и так далее.

Обработка ошибок
Async/await позволяет перехватывать синхронные и асинхронные ошибки одним и тем же кодом, хорошо известным try-catch. Посмотрите, как это выглядит с промисами:

Условия
Написание кода с условиями с async/await намного понятнее.

Стек-фреймы
В отличие от async/await, стектрейс, возвращаемый из цепочки промисов, не дает понимания, где произошла ошибка. Взгляните:

Отладка
Если вы используете промисы, вы знаете, что их отладка — это кошмар. Например, если вы ставите брейкпойнт внутри .then и говорите отладчику встать на этом месте, отладчик не перейдет в .then, поскольку он способен проходить только по синхронному коду. С async/await функции — это обычные синхронные функции, которые отлаживаются без всяких проблем.

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

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

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

У нас есть бесплатный тариф, если хотите попробовать.

Пока я переводил эти четыре статьи, автор написал пятую статью про вебсокеты, HTTP/2 и как с этим работать в JavaScript. Постараюсь перевести к началу следующей недели. Оставайтесь на связи!

Это окончание перевода статьи «How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await». В начале мы разобрались с тем, как вообще работает асинхронный код в JS, как работает петля событий и коллбеки. Пришло время поговорить о том, чем плохи коллбеки и какие есть альтернативы.

Вложенные коллбеки

Посмотрите на код:

Тут у нас цепочка из трех функций, вложенных друг в друга, и каждая представляет один шаг асинхронной серии. Код такого типа часто называется «callback hell». Но это понятие и проблема глубже, чем просто вложение и выравнивание кода.

Сперва мы ждем события «click», затем ждем окончания таймера, и наконец ожидаем ответа AJAX, и повторяем все снова. На первый взгляд, код можно разбить на шаги так:

Итак, такой последовательный способ выражения вашего асинхронного кода кажется намного более естественным, не так ли? Должен быть такой вариант, не так ли?

Промисы

Взгляните на код:

Это очень понятный код: он складывает значения x и y, и печатает результат в консоль. Однако что будет, если значение x или y не задано и будет определено позже? Скажем, нам нужно получить значения с сервера, прежде чем мы сможем их использовать в выражении. Представим, что у нас есть функции loadX и loadY, загружающие значения с сервера. И представим, что у нас есть функция, складывающая значения только тогда, когда оба они доступны.

Это может выглядеть примерно так (немного уродливо):

Обратите внимание вот на что: в этом примере мы определяем x и y как будущие значения, и вызываем операцию sum, которая (внешне) не заботится о том, доступны или нет значения в данный момент.

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

Ценность промисов

Давайте примерно прикинем, как мы можем переписать пример с x + y через промисы:

Здесь мы имеем два уровня промисов.

fetchX и fetchY вызываются напрямую, и значения, которые они возвращают (промисы) передаются в sum(). Нижележащие значения, которые эти промисы представляют, могут быть доступны сейчас или позже, но каждый промис приводит свое поведение к общему знаменателю. Мы рассуждаем о значениях x и y независимо от времени. Это будущие значения, точка.

Второй уровень промисов создается функцией sum() через Promise.all([ . ]), результат которого мы ожидаем через this(). Когда операция sum() закончит работу, мы сможем вывести наше будущее значение в консоль. Логика ожидания будущих значений x и y скрыта внутри sum().

Примечания: Внутри sum() вызов Promise.all([ … ]) создает промис, который ждет, пока promiseX и promiseY вернут значения. Вызов .then() создает отдельный промис, который вернет результат values[0] + values[1] сразу, как только он будет доступен. Таким образом, вызов then() после sum() на самом деле работает с этим вторым промисом, а не с первым, созданным Promise.all([ . ]). Кроме того, этот then() тоже возвращает промис, и мы выбираем, как его использовать. Эта система с цепочками промисов будет объяснена детально чуть ниже.

Для промисов вызов then() на самом деле выполняет две функции: первая — заполнение, как было показано выше, а вторая — отклонение:

Если что-то пошло не так при получении x или y, или что-то еще сломалось в процессе, промис, возвращаемый sum(), будет отклонен, и второй обработчик ошибки, переданный в then, получит отклоненное значение промиса.

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

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

Это действительно полезно, когда вы сцепляете промисы:

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


Обещать или не обещать?

Прим. переводчика: я не люблю делать сноски, указывающие на игру слов, но в оригинальном заголовке игра слов: «To Promise or not to Promise?». Однако я решил не переводить promise как обещание, потому что это уже устоявшийся термин, калькированный с английского.

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

Мы знаем, что промисы создаются как new Promise(…), и вы можете подумать, что проверки p instanceof Promise будет достаточно. Ну, не совсем.

В-основном потому что вы можете получить промис из другого окна (например, iframe), и эта проверка не сработает.

Более того, библиотека или фреймворк могут иметь свою реализацию промисов, несовместимую с нативной реализацией ES6. Фактически, вы можете отлично использовать промисы с библиотеками в старых браузерах, не поддерживающих промисы в принципе.

Поглощение исключений

Если на любом этапе создания или ожидания промиса появляется ошибка JavaScript, например TypeError или ReferenceError, выбрасывается исключение, а промис отклоняется. Например:

Но что произойдет, если промис уже вернул значение, и в коллбеке then() происходит исключение? Несмотря на то, что он не будет потерян, процесс его обработки может немного удивить. Пока вы не заглянете глубже:

Может показаться, что исключение из foo.bar() действительно поглощено. На самом деле нет. Немного глубже произошло событие, которое мы не слушаем. Вызов p.then(…) возвращает новый промис, который будет отклонен с исключением TypeError.

Обработка неперехваченных исключений

Есть другие подходы, которые могут показаться удобнее.

Частое предложение — добавить промисам метод done(), который помечает цепь промисов как законченную. done() не возвращает новый промис, так что коллбеки, переданные в done(), не связаны, чтобы не передавать исключения в несуществующую цепочку.

Проблема решается, когда вы явно ожидаете неперехваченные исключения: каждое исключение внутри обработчика done() будет выброшено как глобальное исключение (обычно в консоль разработчика):

Что будет в ES8? Async/await

JavaScript ES8 представляет async/await, что сделает работу с промисами проще. Мы пробежимся по возможностям, предоставляемым async/await, и тому, как можно их использовать.

Так что давайте посмотрим, как это работает.

Вы определяете асинхронную функцию, используя async. Такие функции возвращают объект AsyncFunction. Он представляет асинхронную функцию, выполняющую код внутри себя.

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

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

Вы можете думать, что промисы в JavaScript — аналог Future в Java или Task в C#. Цель async/await — упростить использование промисов. Давайте посмотрим пример:

Аналогично, функции, выбрасывающие исключение эквивалентны функциям, возвращающим отклоненные промисы:

await используется только внутри async функции, и позволяет вам синхронно ожидать результата промиса. Если вы используете промисы снаружи async, вам все еще понадобится коллбек then:

Вы так же можете определить асинхронную функцию как асинхронное выражение. Это похоже по механизму действия и синтаксису на функцию. Главное отличие между асинхронной функцией и выражением состоит в том, что возможно определять анонимные асинхронные функции. Асинхронное выражение может быть использовано как IIFE (Immediately Invoked Function Expression), которое будет выполнено сразу после определения.

Вот как это выглядит:

Но что важнее всего, async/await поддерживается всеми современными браузерами:

Кроме того, поддерживать совместимость помогают транспилеры вроде Babel и TypeScript.

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

5 советов по написанию поддерживаемого и крепкого аcинхронного кода

Чистый код
Использование async/await позволяет вам писать меньше кода. Каждый раз, используя async/await, вы пропускаете некоторые ненужные шаги: .then, создание анонимной функции для обработки ответа, именование ответа из коллбека и так далее.

Обработка ошибок
Async/await позволяет перехватывать синхронные и асинхронные ошибки одним и тем же кодом, хорошо известным try-catch. Посмотрите, как это выглядит с промисами:

Условия
Написание кода с условиями с async/await намного понятнее.

Стек-фреймы
В отличие от async/await, стектрейс, возвращаемый из цепочки промисов, не дает понимания, где произошла ошибка. Взгляните:

Отладка
Если вы используете промисы, вы знаете, что их отладка — это кошмар. Например, если вы ставите брейкпойнт внутри .then и говорите отладчику встать на этом месте, отладчик не перейдет в .then, поскольку он способен проходить только по синхронному коду. С async/await функции — это обычные синхронные функции, которые отлаживаются без всяких проблем.

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

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

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

У нас есть бесплатный тариф, если хотите попробовать.

Пока я переводил эти четыре статьи, автор написал пятую статью про вебсокеты, HTTP/2 и как с этим работать в JavaScript. Постараюсь перевести к началу следующей недели. Оставайтесь на связи!

В чем разница между Promise ((resolve, reject) => <>) и Promise (resolve => <>)?

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

В чем разница между

Можем ли мы так поступить?

Примеры будут более оценены. Благодаря .

Это не относится к обещаниям, а только к функциям обратного вызова.

new Promise((resolve) => <>); 1 создает Promise, чей обратный вызов принимает только параметр resolve . Невозможно вызвать функцию отклонения, которая в противном случае была бы предоставлена. 2

new Promise((resolve, reject) => <>); создает Promise, чей обратный вызов принимает оба параметра, в том числе тот, который отклоняется.

Вышеприведенные два примера демонстрируют работу позиционных параметров. Первым параметром в функции обратного вызова всегда является функция разрешения, вторая — функция отклонения.

new Promise((reject, resolve) => <>); создаст Promise, в котором вы сможете разрешить с reject и отклонить с resolve .

Вы могли бы throw в области видимости функции обратного вызова или resolve(Promise.reject()) , чтобы вызвать отказ произойдет:

Вы не можете использовать new Promise((resolve) => <>, (reject) => <>); , поскольку конструктор Promise принимает только один аргумент. Вторая функция обратного вызова будет просто проигнорирована.

1: (resolve) => <> , конечно, эквивалентно resolve => <> . Но параметры функции стрелки на самом деле всегда требуют скобок. Простые и одиночные параметры являются единственным исключением, где их можно опустить. См. Статью MDN о синтаксисе функции стрелки.

2: Использование регулярной функции, new Promise(function(resolve)<>); или new Promise(function()<>); вы можете получить доступ к любому аргументу с arguments[0] (разрешить) или arguments[1] (отклонять).

Мастер Йода рекомендует:  Забудьте о matplotlib визуализация данных в Python вместе с plotly
Добавить комментарий