Эволюция асинхронного JavaScript

Функции async уже практически здесь — но дорога к ним была довольно долгой. Не так давно мы писали коллбэки, потом появились Promise / A+ спецификации, следом за ними — функции-генераторы, и теперь async функции.

Давайте взглянем назад и посмотрим, как асинхронный JavaScript развивался с годами. 

Коллбэки

Все началось с коллбэков.

Асинхронный JavaScript

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

Эти так называемые error-first коллбэки лежат в сердце Node.js — модули ядра используют их так же, как и большинство модулей, которые вы можете найти в NPM.

Проблемы с коллбэками:

  • легко написать “callback hell” или спагетти-код, если не использовать их должным образом;
  • легко упустить обработку ошибок;
  • нельзя возвращать значения с выражением return, как и нельзя использовать ключевое слово throw.

В основном из-за этого JavaScript-мир стал искать решения, которые сделали бы асинхронную JavaScript-разработку легче.

Одним из ответов был модуль async. Если вы много работали с коллбэками, то знаете, как сложно может быть заставить что-то, работающее параллельно, работать последовательно; даже маппинг массивов использует асинхронные функции. Тогда благодаря Каолану Макмейхону (Caolan McMahon) родился модуль async.

С async вы можете просто делать так:

Promises

Текущая спецификация JS Promises (в переводе с английского — обещания) берет начало в 2012 и доступна с ES6 — однако обещания не были разработаны сообществом JavaScript. Термин придумал Дэниел Фридман в 1976 году.

Promise представляет собой конечный результат асинхронной операции.

Предыдущий пример с использование обещаний выглядел бы так:

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

При использовании обещаний вы можете использовать полифиллы в средах выполнения, в которых их еще нет. Популярный выбор в таких случаях — bluebird. Эти библиотеки могут предоставлять гораздо большую функциональность, чем нативные обещания — но даже в этих случаях следуйте спецификациям Promises/A+.

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

Хорошо, это довольно-таки легко: единственная вещь, которую вы должны сделать — это обернуть коллбэк в функцию, возвращающую обещание:

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

Или даже проще, вы можете можете начать с интерфейсов обещаний и предоставить совместимость с такими инструментами, как callbackify. Callbackify в основном делает те же вещи, что и предыдущий фрагмент кода, но более общим путем.

Генераторы / yield

Генераторы JavaScript — это относительно новый концепт, который был представлен в ES6.

Разве не было бы здорово, если бы при выполнении функции вы могли бы приостанавливать её в какой-либо точке, рассчитывать что-то, делать другие вещи, а затем возвращаться к ней, возможно, с каким-то значением, и продолжать её выполнение?

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

Если вы хотите легко использовать генераторы для написания асинхронного JavaScript, вам также понадобится пакет co.

С co наши предыдущие примеры могли бы выглядеть так:

Вы можете спросить: а что с операциями, выполняющимися параллельно? Ответ проще, чем вы могли бы подумать (под этой оберткой всего лишь Promise.all):

Async / await

Async-функции были представлены в ES7, и в настоящее время доступны только с транспайлерами, такими как babel (сейчас вы говорим о ключевом слове async, а не о пакете async).

В двух словах, с ключевым словом async мы можем делать то, что мы делали с комбинацией co и генераторов — кроме хаков.

denicola-yield-await-asynchronous-javascript

Под капотом у async функции обещания — поэтому async-функция возвращает тип Promise.

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

Как можете видеть, чтобы использовать async-функцию, вы должны поставить ключевое слово async перед объявлением функции. После этого вы можете увидеть ключевое слово await внутри созданной async функции.

Параллелизм async-функций довольно-таки схож с yield-подходом – за исключением того, что Promise.all не скрыта, но вы должны вызвать её:

Koa уже поддерживает async-функции, так что вы можете опробовать их сегодня, используя babel.


За перевод благодарим нашего подписчика, Александра Хисматулина

Перевод статьи «The Evolution of Asynchronous JavaScript»