Краткая история асинхронных возможностей Javascript

Nikita
WebbDEV
Published in
6 min readJan 28, 2019

--

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

Зачем нужны асинхронные операции?

Компьютерная программа может выполнять неограниченное количество задач. Не секрет что веб-приложения должны работать со множеством различающихся задач, которые, зачастую, должны использовать одни и те же данные. В частности, одним из самых распространённых примеров является вывод информации для пользователя (UI) и получение информации с помощью запросов к серверу. Неудивительно, ведь с этим сталкивается практически каждый веб-разработчик: работа с базой данный, предоставление пользовательского интерфейса, организация некоторого API — все это есть буквально в каждом тестовом задании не только JS программистов.

Почему не выполнять команды последовательно?

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

  1. Получение информации с сайта https:/some/api/item/1
  2. Вывод информации о первом предмете на экран.

возникнут серьезные затруднения с отрисовкой страницы и созданием приятного впечатления на пользователя (так называемый user experience). Просто представьте: странице, скажем, Netflix или Aliexpress придется получить данные сотен баз данных, прежде чем начать отображать содержимое пользователю. Подобная задержка будет подобна загрузке уровня 3D игры, и если игрок готов подождать, то пользователь веб-сайта хочет получить максимум информации в данный момент.

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

В этот момент страничка уже отображается, несмотря на то, что некоторые запросы еще не прошли.

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

ES5 и ранее: Callback

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

Функцией высшего порядка в JS называется функция, принимающая в качестве аргумента другую функцию. Приведем пример:

Таким образом, в функцию высшего порядка — filter — была передана функция objectIsString, позволяющая отфильтровать listOfObjects и оставить в списке только обьекты типа string.
Похожим образом работают и колбэки. Это функция, передаваемая в качестве аргумента другой функции. Чаще всего в качестве примера функции, обрабатывающей callback, приводят функцию setTimeout. В общем виде это используется как setTimeout(function, timeoutValue), где function — это callback функция, исполняемая браузером через период времени, заданный в timeout.

Выведет: 2 1.

ES 6: Обещания (Promises)

В стандарте 6 был представлен новый тип — Promise (обещание, далее — промис). Промис — это тип, объекты которого имеют одно из трех состояний: pending, fulfilled, rejected. Более того, с двумя последними состояниями можно «ассоциировать» функции — коллбэки. Как только асинхронный процесс, описанный в рамках самого промиса придет к успеху/отказу, будет вызвана связанная с этим функция. Этот процесс называют «навешивание коллбэков, и выполняется он с помощью методов then и catch самого промиса. Различие состоит в том, что при вызове then аргументами передаются две функции — на случай успеха (onFullfillment) и провала (onRejected), а catch же принимает, как не трудно догадаться, только функцию для обработки ошибки в промисе. Для того чтобы определить успешно ли выполнен промис в том или ином случае, а так же параметризовать возвращаемый результат

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

Теперь добавим обработчики событий с помощью метода then. Аргументом функции, обрабатывающей успешное завершение, будет result, в то время как аргументом функции для обработки неудачного завершения работы промиса, будет error.

Готово!

Итак, опишем еще раз процесс создания промиса кратко:

  1. Инициализируем объект (new Promise)
  2. Передаем в конструктор в качестве единственного аргумента функцию от resolve и/или reject. В функции должна присутствовать как минимум 1 асинхронная операция
  3. Добавляем с помощью методов then/catch функции — обработчики результата.

Генераторы. Yield

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

Стандартный вид генератора: function* functionName() {}. В теле самих функций для возвращения промежуточного значения используется слово yield.

В качестве примера рассмотрим следующий генератор:

В данный момент генератор находится в начале своего выполнения. При каждом вызове метода генератора next будет выполнен код, описанный до ближайшего yield (или return), а так же будет возвращено значение, указанное в строке с одним из этих слов.

Следующий вызов аналогичным образом вернет значение 2. Третий вызов вернет 3 значение, и закончит исполнение функции.

Несмотря на это, к генератору все еще можно будет обратиться через функцию next. Он, впрочем, будет возвращать одно и то же значение: объект {done: true}.

ES7. Async/await

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

Используя промисы:

С помощью async/await.

Давайте опишем увиденное:

1) Async — ключевое слово, добавляемое при объявлении асинхронной функции
2) Await — ключевое слово, добавляемое при вызове асинхронной функции.

ES8. Асинхронная Итерация

Синхронно итерироваться по данным стало возможно еще в ES5. Спустя две спецификации было решено добавить возможность асинхронной итерации, работающей в асинхронных источниках данных. Теперь при вызове next() возвращаться будет не {value, done}, а промис (см. ES6).

Давайте рассмотрим функцию createAsyncIterable(iterable).

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

Более того, в новом стандарте был определен удобный для подобных операций цикл for-await-of.

TL;DR

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

Если коротко, то:

Callbacks <= ES5
Promises, Yield (Генераторы): ES6
Async/await: ES7
Async Iterators: ES8

Источник: Habr

--

--