Поместить их в свой JS — значит выбросить ребенка и оставить воду в ванне.

JavaScript заслуживает большой похвалы. Как бы мне ни хотелось (и приходится) жаловаться на это, JavaScript функционален, быстр и универсален. Возможно, самым впечатляющим в JavaScript является то, как он превратился из разочаровывающего лоскутного одеяла браузерных вотчин без четкого пути к улучшению в относительно зрелую, быстро развивающуюся экосистему. JavaScript получает новые функции, которые вы можете использовать на большинстве платформ, и вы даже можете зайти на Github и присоединиться к обсуждению их реализации, если хотите. Вдобавок ко всему, JS-транспиляторы больше, чем когда-либо, и только ускорили темпы, с которыми JavaScript-разработчики могут использовать новые языковые функции. Просто посмотрите на JSX! Это практически помещение одного языка внутри другого языка. Насколько мне известно, даже любящее DSL сообщество Ruby не справлялось с этим раньше. И JSX — это не просто побочный проект какого-то случайного хакера. Это дух времени! Microsoft даже создает инструменты для этого.

В целом, я в восторге от будущего JavaScript, и я рад, что вы можете установить Babel и некоторые плагины и использовать возможности из future-JS прямо сейчас. Поскольку я трус с компульсивным недоверием к мнению любого программиста (включая себя), я особенно доволен тем, что многие из этих функций делают JS более безопасным, потому что они делают его более интуитивно понятным. Если ваш проект дает вам возможность установить Babel и начать использовать импорт ES2015, объявления let и const, а также классы, я рекомендую вам это сделать. Приходите и разделите радость превращения JavaScript в настоящий язык программирования, что бы это ни было. Но с моей поддержкой, я должен добавить предостережение. Среди этих милых, интуитивно понятных и достаточно безопасных функций есть два волка в овечьей шкуре. Их имена асинхронны и ждут.

Проблема, которую мы пытаемся решить

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

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

// What is the height of the Bent Pyramid (in meters)?
EgyptDb.pyramidByName('Bent Pyramid', function (p) {
    console.log(p.height);
}, function (err) {
    console.error('Error retrieving pyramid:', err);
}
// => 105

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

function handleError (err) {
    console.error('Error fetching pyramids:', err);
}
EgyptDb.pyramidByName('Bent Pyramid', function (bp) {
    EgyptDb.pharaohById(bp.pharaohId, function (pharaoh) {
        EgyptDb.pyramidById(pharaoh.entombedAt, function (tomb) {
            console.log(tomb.height);
        }, handleError);
    }, handleError);
}, handleError);
// => 105
// Turns out he was very consistent with his pyramid heights

Угу. Это то, что мы называем «кодом пирамиды».

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

Многообещающая альтернатива

Введите обещания. Обещания — это своего рода обратный вызов. С промисами вместо передачи обратного вызова в ваш асинхронный вызов вы вызываете «тогда» для возвращаемого значения вашего асинхронного вызова и передаете туда обратный вызов. Это означает, что вы можете связать обещание за обещанием без отступов. Кроме того, ошибки распространяются по цепочке, поэтому вам не нужно включать обработчик ошибок для каждого вызова! Впрочем, это все немного абстрактно. Позвольте мне показать вам, как они работают, и вы поймете. Вот пример выше с использованием промисов:

EgyptDb.pyramidByName('Bent Pyramid').then(function(bp) {
    return EgyptDb.pharaohById(bp.pharaohId);
}).then(function (pharaoh) {
    return EgyptDb.pyramidById(pharaoh.entombedAt);
}).then(function (tomb) {
    console.log(tomb.height);
}, function (err) {
    console.error('Error fetching pyramid height:', err);
});

Пирамидного кода больше нет! Кроме того, вы можете передать промис и получить от него значение, чего нельзя сделать с помощью обратных вызовов. Сказав это, обещания все еще могут разочаровывать в работе. Их трудно отлаживать, и если вы не будете осторожны, вы можете вернуть обещание чему-то, что ожидает значение. Это особенно неприятно, если у вас есть обещания, которые могут возвращать ложные значения, и вы случайно проверяете истинность этих обещаний вместо их значений. Обещания всегда верны! Также сложно использовать промисы, когда вы хотите использовать результаты двух вызовов одновременно. Одна приятная особенность обратных вызовов заключается в том, что все, что вы уже запросили, все еще находится на вершине пирамиды. В конце концов, обещания — это шаг вперед по сравнению с обратными вызовами, но их все еще сложно сделать правильно. Сообщество JS по-прежнему приветствовало бы более простой способ написания асинхронного кода. Введите асинхронный/ожидание.

Приди, долгожданный фильм

Async/await — это простая функция на первый взгляд. Когда вы пишете функцию, которая делает что-то асинхронное, начинайте ее с «асинхронной функции» вместо «функции». Затем, когда вы вызываете эту функцию в своем коде, просто добавьте к вызову префикс «ожидание», и ваш код остановится и будет ждать результатов этого вызова. Если асинхронный вызов завершится неудачно, он выдаст ошибку так же, как вызов несуществующей функции. Это кажется таким же естественным, как сложение 2 + 2. Самое приятное то, что если у вас есть функции, которые возвращают промисы, вы можете просто поставить перед ними await и использовать их, как если бы они не были асинхронными! Вот пример фараона с async/await:

try {
    const bp = await EgyptDb.pyramidByName('Bent Pyramid');
    const pharaoh =
        await EgyptDb.pharaohById(bp.pharaohId);
    const tomb =
        await EgyptDb.pyramidById(pharaoh.entombedAt);
    console.log(tomb.height);
} catch (err) {
    console.error('Error fetching pyramid height:', err);
}

Это вообще не выглядит асинхронным! И все же это так. Если есть ошибка, она не исчезает, как это иногда случается с цепочками обещаний. Вы можете поймать его или позволить ему пузыриться, но в любом случае вы его заметите. Это оригинальная обработка ошибок JavaScript. Это кажется таким естественным. С таким же успехом вы можете писать на Ruby или Python! Итак, если вы похожи на меня, вы, вероятно, охотно наполните свой новый код этими конструкциями, как только узнаете о них. А потом… что ж, вы узнаете о них больше, чем когда-либо хотели.

Это чувство асинхронности

Вскоре медовый месяц закончится. В конце часа, потраченного на отладку печати новой конечной точки, которую вы пишете, вы посмотрите на свой код с новой блестящей функцией JavaScript и удивитесь, где вы ошиблись. Я умоляю вас не винить себя. Вы были просто обмануты обещанием (или это обратный вызов?), что что-то остановит безумие в вашей жизни, которое исходит от JavaScript. Вам явилась языковая функция и сказала: «Следуйте за мной в удобное, синхронное прошлое, которое у вас было до JavaScript», ну и как вы могли ей не следовать? Это все, что вы когда-либо хотели, верно?

К сожалению, Async/await — это не решение. Или, скорее, это краткосрочное решение долгосрочных проблем, которое может быть хуже, чем отсутствие решения вообще. Что может пойти не так, если вы используете async и await?

Вы забываете, что вы передаете вокруг

Я упоминал ранее, что вы должны быть осторожны с промисами, потому что их легко спутать со значениями, которые они выдают. Асинхронные функции фактически возвращают промисы (в реализации Babel), поэтому у них та же проблема. Забудьте об ожидании вашего вызова асинхронной функции, и вместо этого вы получите обещание. И даже если это обещание обернуто неопределенным, оно все равно верно. Это не новая проблема, но она немного усугубляется асинхронностью и ожиданием, потому что асинхронные функции не возвращают промис. Например, если вы хотите получить список друзей пользователя и красиво его отформатировать, это может выглядеть так:

// With promises
function prettyFriendsList (user) {
    return user.getFriends().then(function (friends) {
        // Remove some sensitive data
        // Calculate some numbers
        // Reticulate splines
        // Zhoosh up the presentation and store them in friendList
        return Promise.resolve(friendList);
    });
}
// With promises
async function prettyFriendsList (user) {
    const friends = await user.getFriends();
    // Remove some sensitive data
    // Calculate some numbers
    // Reticulate splines
    // Zhoosh up the presentation and store them in friendList
    
    return friendList;
}

Где вы должны искать в функции, чтобы узнать, получаете ли вы значение или обещание? Для многообещающей функции вы ищете слово «возврат». Независимо от того, смотрите ли вы на то, что возвращается внутри функции then или за ее пределами, вы придете к одному и тому же выводу: она возвращает обещание. Для асинхронной функции вы должны посмотреть вверху и заметить слово асинхронный. Не ищите оператор возврата. Это не может вам помочь. Это не имеет большого значения для небольших функций, но для более крупных? Вы можете пропустить это. Я видел, как это происходит, и я сделал это. Это раздражает. Но если вы достаточно сообразительны, чтобы правильно экранировать функции, которые вы вызываете, то вас, вероятно, больше волнует тот факт, что async и await

Неровные обои вместо обещаний

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

Пользователь по имени coldtea указал в комментарии к Hacker News, что Promises — это [костыль] из-за отсутствия встроенной языковой функции, а async/await — это то. Казалось бы, это опровергает мое утверждение о том, что async/await — это тонкий фасад. Может быть, однажды мы достигнем этой точки. Но пока async/await просто прикрывает промисы. Если это изменится, это может сломать ваш код. И мы начнем заново, ожидая, пока ваши любимые библиотеки примут конвенцию. В любом случае, если вы беспокоитесь о написании элегантного JavaScript, это не будет вас беспокоить так сильно, как это.

Он отказывается от одной из самых сильных сторон JavaScript

Асинхронность — секретный соус JavaScript. Это огромная часть того, что делает JavaScript на сервере стоящим. Node.js не украл умы и долю рынка у Ruby и Python, потому что JavaScript красив. Это произошло потому, что одновременное выполнение множества мелких задач позволяет вам запускать быстрый сервер, а JavaScript хорош для выполнения множества действий одновременно. Async и await выбросьте это. Когда ваш код сталкивается с ожиданием, вы ждете, пока это произойдет, независимо от того, зависит ли код после него от этого события. Возможно, вы сэкономите время на размышлениях о том, какие части вашего кода могут выполняться параллельно, но при этом вы никогда ничего не будете запускать параллельно. JavaScript становится синхронным. Но если вам нужен полностью синхронный язык, у вас есть много вариантов. Многие из них более читабельны, а некоторые даже содержат целые числа. Большинство из них сообщат вам о возникновении ошибки с большим удовольствием и подробностями, чем JavaScript. Вы, вероятно, должны использовать один из них вместо этого. Это не значит, что JavaScript не может черпать вдохновение из других языков. Классы, я думаю, прекрасное дополнение. Но ожидание — это больше, чем просто заимствование функции. Это поощряет стиль программирования и образ мышления, которые выявляют недостатки JavaScript, а не его сильные стороны.

Если вы не хотите писать JavaScript (не совсем непонятное чувство), но должны, то я понимаю, что вы используете асинхронность и ожидание. Это приближает вас к языку, который вы хотите использовать, в то время как вы пишете на языке, который вам нужно использовать. Я просто советую вам действовать осторожно и, если вы готовы, узнать больше о том, что async и await делают за кулисами, чтобы вы не получили неизвестные неизвестные.

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

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

Примечание. Async/await по-прежнему является всего лишь предложением для ECMAScript и реализуется через промисы в Babel. Вполне возможно, что к тому времени, когда async/await появится в браузерах, он будет реализован по-другому, что может смягчить или полностью решить мою вторую жалобу. Однако я склонен сказать, что мой третий останется, за исключением серьезных изменений синтаксиса.