Обещания: полное руководство
Не так мощно, как вы думаете
Я не хочу называть имен, но многие статьи, которые я читал об асинхронности в JavaScript, особенно те, которые вышли примерно в то время, когда ES2015 (ES6) был новым, просто неверны. Я собираюсь развенчать ложь, представив реальное понимание того, как асинхронный режим работает в JavaScript.
Эта серия статей основана на слайде из моего выступления о Redux-Observable. Я хотел пойти еще дальше и объяснить, как работает асинхронный код и почему он лежит в основе кодирования на JavaScript.
Наименее способный метод
Обещания удивительны, но крайне ограничены. Я много делал с обещаниями за эти годы, даже до того, как они стали популярными как часть нативного JavaScript в 2015 году. Мне больше всего в них нравится то, как они сделали асинхронность намного проще для понимания. Он устранил ад обратных вызовов и уменьшил косвенное обращение, вызванное обещаниями, за счет предоставления линейного API, который можно было читать сверху вниз.
Вроде.
Так почему же обещания «наименее эффективны»? Потому что у них так много недостатков. По мере использования обещаний вы увидите больше этих недостатков и будете стремиться к их исправлению. И хотя я использовал Bluebird раньше, он не решает всех проблем; он просто делает обещания более универсальными.
Путаница
В свое время существовало множество различных реализаций обещаний. Я был хорошо знаком с версией jQuery и сильно настроил ее, чтобы сделать что-то вроде многократного запуска данных через конвейер. Он не соответствовал спецификации A +, но в конечном итоге предоставил методы, необходимые для этого в версии 1.8.
AngularJS был таким же. Когда он вышел, в нем использовалась принципиально отличная методология от того, что мы знаем сегодня как обещания. Версия 1.6 открыла двери для обещаний A +, а версия 1.7 полностью покончила со старыми методами!
И вы, вероятно, знакомы или слышали о Bluebird. Это фантастическая библиотека, которая добавляет множество функций, которые в настоящее время отсутствуют в реализации обещаний ES2015. Даже версия ES2018 добавляет только Promise.prototype.finally
; ничего подобного тому, что делает доступным Bluebird.
И снова, строго говоря, в обещаниях, Bluebird - это библиотека для вас. Но есть не только обещания. В JavaScript существуют и другие асинхронные методы, и хотя обещания прекрасны, их заменяют наблюдаемые. Тем не менее, когда дело доходит до толчка, A + Promise
является родным для ES2015, который имеет четкую шляпу ко всему, что требует сторонней библиотеки, например, наблюдаемых.
Решить или отклонить? Вот в чем вопрос.
На самом деле это не вопрос, который кто-либо когда-либо задавал в своей жизни. Что это за методы и как к ним получить доступ?
Если вы создаете обещание на основе обратного вызова, вы создадите Promise
вот так:
new Promise
принимает обратный вызов, поэтому, как и в случае с обратными вызовами, вам нужно поместить свой _5 _-_ 6_ код в обратный вызов, а не за его пределами.
Удивительно, но при создании нового обещания вы не только выполняете обратный вызов, но и получаете два обратных вызова в вашей функции обратного вызова. Это то, что я имел в виду в Обратные вызовы: полное руководство: все асинхронные функции в JavaScript основаны на обратных вызовах. resolve
дает полезный результат, а reject
выдает ошибку в цепочку обещаний, но не возвращает ошибку. Подробнее об этом в следующем разделе.
Но я редко бываю в новинку Promise
. Вместо этого я выбираю семантические Promise.resolve
и Promise.reject
. Иногда они не соответствуют вашим потребностям, как в приведенном выше примере, или если вы конвертируете из обратного вызова, но они отлично подходят для отладки и очень полезны, когда вам нужно создавать обещания из плоских значений. Это похоже на прямой вызов resolve
и reject
обратных вызовов.
Выглядят они примерно так:
Потомственный
Обещания называются доступными, что означает, что вы можете связать несколько then
операторов, и каждый из них получает значение, возвращаемое предыдущим обратным вызовом. Как вы помните, в моей предыдущей статье о обратных вызовах я говорил, что можно получить возвращаемое значение обратного вызова. Таким образом, вместо вызова другого обратного вызова, если вы сохраните свои обратные вызовы синхронными, вы сможете получить от них возвращаемые значения.
Это именно тот процесс, который использует then
функция обещания. Он принимает обратный вызов (также известный как обработчик) и выполняет его синхронно, принимает это возвращаемое значение и передает его следующему then
в цепочке. Если какой-либо из этих обратных вызовов ошибается, он будет перемещаться по цепочке, пока не найдет catch
(или второй аргумент then
), и вызовет свой метод обратного вызова.
Что происходит после «улова»?
Это странная часть для большинства людей. Я гарантирую, что большинство разработчиков JavaScript не знают эту информацию.
Если вы throw
ошибка из catch
, а где-то ниже по цепочке есть еще catch
, второй перехватчик получит эту ошибку. Но допустим, вы не выдаете ошибку, а просто console.error
. Жаль, теперь вы попадете на следующий then
в цепочке.
Так что, хотя это довольно круто, вы можете продолжить обработку ошибки, но хуже всего то, что вы продолжаете обрабатывать ее вечно, во веки веков. Невозможно остановить обещание, так как любое then
будет вызываться, если вы не прошли все из них, и любая эта ошибка попадет в catch
, который может перейти к следующему catch
или then
.
Это меня привлекало в прошлом и является одной из основных причин, по которой я перестал использовать обещания в производственном коде (теперь я использую наблюдаемые). Ситуация усугубляется, если все ваше приложение состоит из длинной цепочки из множества передаваемых обещаний. Это может быть довольно непросто, но, по крайней мере, ваше приложение не перестанет обрабатывать! 👺
Два аргумента then или catch?
Метод then
обещания может принимать два аргумента. Я рекомендую указывать только один, потому что второй аргумент буквально такой же, как после then
с catch
.
Возьмем, к примеру, эти два примера:
Это в точности то же самое, но лучше читается, проще в обслуживании и с меньшей вероятностью будет упущено при отладке. Поверьте, вы хотите, чтобы на вашем пути было как можно меньше сумасшедшего кода, когда вы имеете дело со сложными асинхронными ошибками.
Если по какой-либо причине у вас в then
очень длинные анонимные функции, вы всегда будете пропускать второй аргумент. Это все разработчики в вашей команде, включая вас через 6 месяцев.
Честно говоря, делайте то, что лучше всего подходит вашей команде в вашем проекте, но я предпочитаю семантический catch
несемантическому двойному аргументу then
, и я действительно думаю, что вы тоже должны. Я бы никогда не позволил другому разработчику использовать версию с двумя аргументами без смехотворно веской причины.
Возвращение обещаний
Чтобы избежать ада обратных вызовов, важно отметить, что then
может обрабатывать возврат как значения, так и другого обещания. При возврате значения оно передается следующему then
, но при возврате обещания фактически переключает ваше следующее then
на значение, которое обещание передает по цепочке; не само обещание.
Вот как это выглядит:
Обратите внимание на то, как вы можете вернуть как выполненные, так и отклоненные обещания, и они попадут в следующие then
или catch
, как обычно. Это мощная функция, поскольку она позволяет постоянно держать свои обещания в большей части с вкладками слева.
Обратный звонок-ад
Я действительно видел, как люди писали обратный вызов с обещаниями вроде этого:
А затем, как умный разработчик обещаний, вы войдете и исправите это:
Проблема возникает, когда у вас есть проблемы с определением объема. Например, вы могли использовать объекты server и connection в своей функции сообщения, поскольку вам нужно знать, на какое соединение отправлять ответ и какой сервер был вызван. Несмотря на то, что вы очистили код, вы потеряли область действия предыдущих значений.
Есть несколько способов решить эту проблему, но один из самых чистых (вроде) может заключаться в том, чтобы каждый раз просто объединять их все в новый объект:
Я использовал эту методологию раньше, но никогда не любил ее. Это удобно, когда вам нужно разделить вещи, потому что ваши then
обратные вызовы становятся слишком большими. Хотя это позволяет избежать ада обратных вызовов, можно спорить, действительно ли это лучше.
Это точно такая же ситуация, о которой я говорил в Обратные вызовы: Полное руководство. Обратный звонок - это не обязательно плохо. Даже если вам говорят, что обещания обходят это стороной, на самом деле это не так.
Антипаттерны
Как и в случае с обратными вызовами, обещания не работают с _40 _-_ 41_, не так, как большинство людей их использовали бы.
Вот антипаттерн, который вы обычно видите:
Это должно быть записано как:
Почему это? Потому что обещания всегда асинхронны. Абсолютно невозможно сделать обещание синхронным, как ванильные обратные вызовы. Это важно знать, потому что отказ от метода Promise.prototype.catch
поможет вам. Обычно я сталкиваюсь с такими проблемами в приложениях Node.js, поскольку большинство разработчиков браузеров часто используют Promise.prototype.catch
.
Наконец-то
Еще до ES2018 у нас не было столь необходимого finally
метода, теперь он у нас наконец. Упрощенно, все, что делает этот метод, - это добавление синтаксического сахара к паре then
и catch
, но без передачи значений. Это похоже на оператор finalize
в RxJS, который, к сожалению, также не принимает значений. Учитывая количество обещаний, которые я видел, вызывая проблемы без него, я бы сказал, что это долгожданное дополнение к ES2018.
Повторюсь, finally
обратные вызовы не получают значений. Это не похоже на tap
в RxJS, это метод, который помогает только с очисткой, независимо от того, был ли код успешным или ошибочным.
Вот как это выглядит:
Возможно, это не то, что вы ожидали, но каждый раз, когда передается finally
, он выполняется. Поэтому, если вы поместите его перед связкой связанных операторов then
и catch
, он выполнится перед ними. Если не then
, он также будет выполняться в случае ошибки. Обычно они переходят к следующему catch
, но они также будут выполнять все finally
функции на этом пути.
В реальных приложениях, если у вас нет finally
, возможно, вы создаете кучу утечек памяти и бесконечные индикаторы загрузки и т.д. then
. Если в вашем обещании возникает ошибка, а последний then
убирает индикатор загрузки, он никогда не исчезнет.
Хотя finally
не обязательно должно быть в конце обещания, его наличие позволяет вам использовать его вместо then
и гарантировать, что ваше приложение очистится после того, как оно будет выполнено.
Попробуй-поймай-наконец
Если вы еще не знали, _62 _-_ 63_ также имеет finally
метод. Это выглядит так:
В этом случае он должен быть в порядке try
, затем catch
, затем finally
. Можно полностью исключить catch
или finally
, но не оба сразу.
Как видите, он будет выполняться независимо от того, нажмете ли вы try
или catch
. Итак, почему это существует? Вы могли бы просто войти после вашего _72 _-_ 73_ вместо использования finally
. И это важный момент. Если ваш catch
выдает ошибку, ваше выполнение сразу же остановится, и ваш console.log
никогда не произойдет. finally
to гарантирует, что вы все равно сможете выполнить что-то даже после выдачи ошибки.
Тогда почему бы вам просто не поместить этот код в catch
вместо finally
? Потому что вы также хотите, чтобы этот код был в вашем try
. Это предотвращает дедупликацию кода и гарантирует, что ваш код всегда будет выполняться, пока существует этот оператор _81 _-_ 82 _-_ 83_.
Обещания используют finally
больше как более чистую вспомогательную функцию, но в обоих случаях у вас все еще может быть открыто соединение с базой данных, которое вам нужно закрыть. Здесь он будет выполнен (да, даже после выдачи ошибки в catch
), чтобы закрыть это соединение. Это незаменимо в таких сценариях.
Все для расы и гонка для всех
Если вы не знали, в API обещаний скрыты еще два метода: Promise.race
и Promise.all
. Оба они являются методами обработки Array
обещаний, точнее говоря, итератора обещаний, но не будем вдаваться в подробности.
Все
Promise.all
берет этот список обещаний, ждет, пока все они вернут значение, и отправляет массив этих значений следующему обратному вызову then
:
При использовании Promise.all
убедитесь, что вы передаете массив обещаний, поскольку он принимает только один аргумент. Ваши результаты также возвращаются в массив, где каждое значение является разрешенным значением из предыдущего.
Если таковые имеются, и я имею в виду, что любое одно обещание не выполняется, весь ваш Promise.all
находится в отклоненном (ошибочном) состоянии, и ваше сообщение об ошибке относится только к первому ошибочному обещанию. Если у вас более одной ошибки, у вас нет возможности узнать.
Гонка
Promise.race
берет этот список обещаний и берет первое, чтобы передать значение. Если у вас их будет несколько, остальные будут практически отменены:
Это, вероятно, наиболее распространенный вариант использования Promise.race
. Я сам нечасто использовал этот оператор, но у него тоже есть подводные камни. Во-первых, вы должны не забыть передать ему массив точно так же, как Promise.all
. Во-вторых, если одна из этих ошибок обещает ошибку, это не имеет значения, если только эта ошибка не возникнет первой.
Таким образом, если у вас есть два обещания, одно выполнено успешно, а другое - ошибочно, вы получите значение успеха. Если у вас есть два обещания и одна ошибка до того, как другая будет успешной, ваш Promise.race
будет в отклоненном состоянии. Это действительно большая причуда и, вероятно, причина многих часов разочарования у людей, использующих Promise.race
.
Синяя птица
Существует множество библиотек обещаний, но, безусловно, самой популярной и, вероятно, той, которую вы действительно увидите в продакшене, является Bluebird. Это фантастическая библиотека обещаний с множеством функций, но, если вы не работаете в области Node.js, вы, вероятно, ее не увидите.
И нативные обещания JavaScript - это не обещания Bluebird, поэтому вам придется их преобразовывать.
Использование библиотеки дает немало преимуществ, лучшая из которых - функция Bluebird.promisify
.
Лично я не рекомендую следовать официальным рекомендациям Bluebird для импорта его в свой проект, и я написал об этом целую статью для вашего удобства:
Документы о плохой практике Bluebird
Множество недостатков
Все, о чем я говорил, в основном о профи, но я дал этой статье подзаголовок с мыслью, которая обещает отстой. Почему? Что может быть такого плохого, если они выглядят лучше, чем обратные вызовы?
Один и готово
Во-первых, их можно выстрелить только один раз. Обратные вызовы превосходят обещания именно по этой причине, поскольку они могут обрабатывать события с течением времени. Обещания страдают от того, что они ограничиваются одной эмиссией, и это смертельный недостаток, с которым я сталкивался во многих случаях. Думаю, это всплывает каждый раз, когда я работаю с обещаниями в крупномасштабных приложениях.
Отладка болезненна
Консольное ведение журнала (подключение к потоку) сложно. Отладка обещаний - отстой. Вы можете подумать, что Promise.prototype.finally
- это именно то, что мы искали, но он не получает никаких данных из цепочки обещаний, поэтому его нельзя использовать для ведения журнала. У Bluebird тоже нет решения для этого.
Одна из самых сильных сторон наблюдаемых RxJS - это ваша способность подключаться к потоку в любой момент его работы. Каждый Promise.prototype.then
эквивалентен как map
, так и switchMap
, где вы можете вернуть либо значение, либо другое обещание. К сожалению, это изменяет значение, передаваемое в цепочке, и требует более сложной логики для добавления туда некоторых отладочных операторов, логики, которая может вызвать головную боль при обслуживании, если все сделано неправильно.
Я делаю это с помощью читов, только при отладке, вот так:
Он продолжается и продолжается ...
Обещания не перестают выполняться. Это проблема при обнаружении ошибок и then
поиске после них. Они идут по цепочке, пока ничего не остается, и у них нет абсолютно никакой возможности остановиться на полпути.
Вы можете присвоить значение «остановить обработку» самому объекту обещания, но тогда вам придется либо расширить класс Promise
, либо заключить каждый then
, catch
и finally
в функцию «проверить, остановлено ли обещание».
Немедленное исполнение
Обещания выполняются сразу после создания. Хотя это может быть неочевидно, если у вас есть setTimeout
в объявлении обещания, оно будет выполнено, как только оно появится, а не ждать, пока появится ваш первый then
. Если вы этого не ожидаете, это может навредить. Единственный способ обойти это - создать функцию, которая затем возвращает обещание, заключенное в setTimeout
.
Никогда не синхронно
Обещания никогда не бывают синхронными даже с Promise.resolve
. Это означает, что они не могут быть подключены как собственный оператор конвейера, пока мы ждем официальной реализации ES.
Подробнее об этом в моей статье:
Руководство по функциональному программированию для любителей эмодзи: часть 4
Не удаляет полностью Callback-Hell
Точно так же обратные вызовы требуют закрытия, чтобы избавиться от обратного вызова, обещания должны отображать возвращаемые данные в более сложный объект. Из-за переменной области видимости легко попасть в такую же аду обратных вызовов, как обратные вызовы.
Трудно обойти обещания в `then`
Если вы действительно хотите передать обещание, а не его значение, следующему then
, вам придется заключить его в массив или объект, чтобы оно не преобразовывалось автоматически. Большинство людей не осознают этого и, вероятно, потратят несколько часов, пытаясь выяснить, почему их код не работает. Я знаю, что есть.
Семантика
then
не является семантическим. Конечно, это семантическое выражение «это случается, then
это случается», но я не это имею в виду. Каждая функция, которую вы передаете в then
обещания, является либо анонимной (т. Е. Безымянной), либо именованной функцией, которая существует где-то еще в вашем приложении. Это вызывает такое же косвенное обращение, как и в обратных вызовах, когда обещания лишь немного лучше из-за линейности операций.
Заключение
Если все, что у меня есть, - это обещания, я воспользуюсь ими. Они намного предпочтительнее обратных вызовов в моей книге и были огромным шагом к тому, чтобы помочь мне управлять асинхронностью в JavaScript в течение трех лет, когда я застрял с ними, но во всем, что я написал за последний год и даже больше, я вместо этого выбрали наблюдаемые, поскольку они хорошо выходят за рамки того, что можно ожидать от обещаний.
Чем проще ваши обещания, тем легче с ними работать. Чем длиннее ваши обещания и чем чаще вы их передаете в приложении, тем труднее их отслеживать и тем менее надежными они становятся.
Мне нравятся обещания?
Да.
Я считаю, что наблюдаемые лучше?
Конечно.
Если я использую обратные вызовы, буду ли я их обещать?
Наверное, больше нет.
Больше Чтений
У меня скоро появятся новые статьи по асинхронности! Предыдущий был с обратными вызовами:
Обратные вызовы: полное руководство
Если вам понравилось то, что вы прочитали, ознакомьтесь с другими моими статьями на похожие интересные темы: