По мере изучения Javascript я раз за разом натыкался на многочисленные статьи о асинхронных функциях и операциях. Несмотря на несомненные достоинства подобного функционала, каждый раз в затруднение меня вводил листинг, приводимый авторами. Слова менялись, суть оставалась той же, в голове заваривалась каша. Под катом — небольшой гайд по историческому развитию и версиям ECMA.
Компьютерная программа может выполнять неограниченное количество задач. Не секрет что веб-приложения должны работать со множеством различающихся задач, которые, зачастую, должны использовать одни и те же данные. В частности, одним из самых распространённых примеров является вывод информации для пользователя (UI) и получение информации с помощью запросов к серверу. Неудивительно, ведь с этим сталкивается практически каждый веб-разработчик: работа с базой данный, предоставление пользовательского интерфейса, организация некоторого API – все это есть буквально в каждом тестовом задании не только JS программистов.
Почему не выполнять команды последовательно?
Зачастую информация, необходимая пользователю, может быть получена лишь через значительный отрезок времени. Если организовать программу как:
возникнут серьезные затруднения с отрисовкой страницы и созданием приятного впечатления на пользователя (так называемый user experience). Просто представьте: странице, скажем, Netflix или Aliexpress придется получить данные сотен баз данных, прежде чем начать отображать содержимое пользователю. Подобная задержка будет подобна загрузке уровня 3D игры, и если игрок готов подождать, то пользователь веб-сайта хочет получить максимум информации в данный момент.
Решение было найдено: асинхронные операции. Пока основной поток программы занят инициализацией и выводом на канвас элементов веб-сайта, он так же выводит в другие потоки задачи в духе «получиТоварыДляПользователя». Как только этот поток завершает свою работу, информация «оседает» в главном потоке, и становится доступной для отображения, а на самой веб-странице находится определенный placeholder – объект, занимающий место для будущей информации.
В этот момент страничка уже отображается, несмотря на то, что некоторые запросы еще не прошли.
Скорее всего, где-то внизу страницы еще несколько запросов возвращают значение, и страничка продолжает обновляться и отрисовываться динамически, без неудобств для пользователя.
Перед тем, как приступить к рассмотру колбэков давайте еще раз взглянем/узнаем, что такое функции высшего порядка.
Функцией высшего порядка в JS называется функция, принимающая в качестве аргумента другую функцию. Приведем пример:
Таким образом, в функцию высшего порядка – filter — была передана функция objectIsString, позволяющая отфильтровать listOfObjects и оставить в списке только обьекты типа string.
Похожим образом работают и колбэки. Это функция, передаваемая в качестве аргумента другой функции. Чаще всего в качестве примера функции, обрабатывающей callback, приводят функцию setTimeout. В общем виде это используется как setTimeout(function, timeoutValue), где function – это callback функция, исполняемая браузером через период времени, заданный в timeout.
Выведет 2 1.
В стандарте 6 был представлен новый тип – Promise (обещание, далее – промис). Промис – это тип, объекты которого имеют одно из трех состояний: pending, fulfilled, rejected. Более того, с двумя последними состояниями можно «ассоциировать» функции – коллбэки. Как только асинхронный процесс, описанный в рамках самого промиса придет к успеху/отказу, будет вызвана связанная с этим функция. Этот процесс называют «навешивание коллбэков, и выполняется он с помощью методов then и catch самого промиса. Различие состоит в том, что при вызове then аргументами передаются две функции – на случай успеха (onFullfillment) и провала (onRejected), а catch же принимает, как не трудно догадаться, только функцию для обработки ошибки в промисе. Для того чтобы определить успешно ли выполнен промис в том или ином случае, а так же параметризовать возвращаемый результат
Давайте поэтапно создадим и используем промис.
Теперь добавим обработчики событий с помощью метода then. Аргументом функции, обрабатывающей успешное завершение, будет result, в то время как аргументом функции для обработки неудачного завершения работы промиса, будет error.
Готово!
Итак, опишем еще раз процесс создания промиса кратко:
Также в стандарте ES6 был определен новый вид функций – генераторы. Эти функции имеют возможность при идентичных на первый взгляд вызовах несколько раз возвращать разное значение. Давайте разберемся, как они это делают и зачем этим пользоваться.
Стандартный вид генератора: function* functionName() {}. В теле самих функций для возвращения промежуточного значения используется слово yield.
В качестве примера рассмотрим следующий генератор:
В данный момент генератор находится в начале своего выполнения. При каждом вызове метода генератора next будет выполнен код, описанный до ближайшего yield (или return), а так же будет возвращено значение, указанное в строке с одним из этих слов.
Следующий вызов аналогичным образом вернет значение 2. Третий вызов вернет 3 значение, и закончит исполнение функции.
Несмотря на это, к генератору все еще можно будет обратиться через функцию next. Он, впрочем, будет возвращать одно и то же значение: объект {done: true}.
Вместе со стремлением угодить любителям ООП с помощью синтаксического сахара классов и имитации наследований, создатели ES7 пытаются облегчить понимание javascript и для любителей писать синхронный код. С помощью конструкций async/await пользователь имеет возможность писать асинхронный код максимально похожий на синхронный. При желании можно избавиться от недавно изученных промисов и переписать код с минимальными изменениями.
Рассмотрим пример:
Используя промисы:
С помощью async/await.
Давайте опишем увиденное:
1) Async – ключевое слово, добавляемое при объявлении асинхронной функции
2) Await – ключевое слово, добавляемое при вызове асинхронной функции.
Синхронно итерироваться по данным стало возможно еще в ES5. Спустя две спецификации было решено добавить возможность асинхронной итерации, работающей в асинхронных источниках данных. Теперь при вызове next() возвращаться будет не {value, done}, а промис (см. ES6).
Давайте рассмотрим функцию createAsyncIterable(iterable).
Как видим, функция инициализирует коллекцию, для каждого обращения к элементам которой будет возвращен промис со значением, указанным в iterable.
Более того, в новом стандарте был определен удобный для подобных операций цикл for-await-of.
Вовсе не обязательно знать и наизусть помнить к какой версии ECMAScript относится тот или иной синтаксис, особенно если вы только начали своё знакомство с асинхронным поведением в JS. В то же время, изучение асинхронности именно в порядке, предлагаемом историей развития спецификаций позволит программисту не только в совершенстве понять синтаксис и инструкции, передаваемые JS движку, но и проследить логику совершенствования ECMAScript как продукта, понять тенденции, диктуемые JS разработчиками, разделить их и принять.
Если коротко, то:
Callbacks <= ES5
Promises, Yield (Генераторы): ES6
Async/await: ES7
Async Iterators: ES8
Зачем нужны асинхронные операции?
Компьютерная программа может выполнять неограниченное количество задач. Не секрет что веб-приложения должны работать со множеством различающихся задач, которые, зачастую, должны использовать одни и те же данные. В частности, одним из самых распространённых примеров является вывод информации для пользователя (UI) и получение информации с помощью запросов к серверу. Неудивительно, ведь с этим сталкивается практически каждый веб-разработчик: работа с базой данный, предоставление пользовательского интерфейса, организация некоторого API – все это есть буквально в каждом тестовом задании не только JS программистов.
Почему не выполнять команды последовательно?
Зачастую информация, необходимая пользователю, может быть получена лишь через значительный отрезок времени. Если организовать программу как:
- Получение информации с сайта https:/some/api/item/1
- Вывод информации о первом предмете на экран.
возникнут серьезные затруднения с отрисовкой страницы и созданием приятного впечатления на пользователя (так называемый user experience). Просто представьте: странице, скажем, Netflix или Aliexpress придется получить данные сотен баз данных, прежде чем начать отображать содержимое пользователю. Подобная задержка будет подобна загрузке уровня 3D игры, и если игрок готов подождать, то пользователь веб-сайта хочет получить максимум информации в данный момент.
Решение было найдено: асинхронные операции. Пока основной поток программы занят инициализацией и выводом на канвас элементов веб-сайта, он так же выводит в другие потоки задачи в духе «получиТоварыДляПользователя». Как только этот поток завершает свою работу, информация «оседает» в главном потоке, и становится доступной для отображения, а на самой веб-странице находится определенный placeholder – объект, занимающий место для будущей информации.
В этот момент страничка уже отображается, несмотря на то, что некоторые запросы еще не прошли.
Скорее всего, где-то внизу страницы еще несколько запросов возвращают значение, и страничка продолжает обновляться и отрисовываться динамически, без неудобств для пользователя.
ES5 и ранее: Callback
Перед тем, как приступить к рассмотру колбэков давайте еще раз взглянем/узнаем, что такое функции высшего порядка.
Функцией высшего порядка в JS называется функция, принимающая в качестве аргумента другую функцию. Приведем пример:
objectIsString(objectRef) {
return typeof(objectRef) === ‘String’;
}
listOfObjects.filter(objectIsString);
Таким образом, в функцию высшего порядка – filter — была передана функция objectIsString, позволяющая отфильтровать listOfObjects и оставить в списке только обьекты типа string.
Похожим образом работают и колбэки. Это функция, передаваемая в качестве аргумента другой функции. Чаще всего в качестве примера функции, обрабатывающей callback, приводят функцию setTimeout. В общем виде это используется как setTimeout(function, timeoutValue), где function – это callback функция, исполняемая браузером через период времени, заданный в timeout.
setTimeout(console.log(1), 2000);
console.log(2);
Выведет 2 1.
ES 6: Обещания (Promises)
В стандарте 6 был представлен новый тип – Promise (обещание, далее – промис). Промис – это тип, объекты которого имеют одно из трех состояний: pending, fulfilled, rejected. Более того, с двумя последними состояниями можно «ассоциировать» функции – коллбэки. Как только асинхронный процесс, описанный в рамках самого промиса придет к успеху/отказу, будет вызвана связанная с этим функция. Этот процесс называют «навешивание коллбэков, и выполняется он с помощью методов then и catch самого промиса. Различие состоит в том, что при вызове then аргументами передаются две функции – на случай успеха (onFullfillment) и провала (onRejected), а catch же принимает, как не трудно догадаться, только функцию для обработки ошибки в промисе. Для того чтобы определить успешно ли выполнен промис в том или ином случае, а так же параметризовать возвращаемый результат
Давайте поэтапно создадим и используем промис.
//Обьявим переменную:
let promise;
//Определим переменную как объект подтипа Promise.
let promise = new Promise((resolve, reject) => {
});
//Заполним функцию промиса, выполняемую асинхронно.
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("result");
}, 1000);
});
Теперь добавим обработчики событий с помощью метода then. Аргументом функции, обрабатывающей успешное завершение, будет result, в то время как аргументом функции для обработки неудачного завершения работы промиса, будет error.
promise
.then(
result => {
},
error => {
}
);
// Определим непосредственно работу функций – обработчиков.
promise
.then(
result => {
// первая функция-обработчик - запустится при вызове resolve
alert("Fulfilled: " + result); // result - аргумент resolve
},
error => {
// вторая функция - запустится при вызове reject
alert("Rejected: " + error); // error - аргумент reject
}
);
Готово!
Итак, опишем еще раз процесс создания промиса кратко:
- Инициализируем объект (new Promise)
- Передаем в конструктор в качестве единственного аргумента функцию от resolve и/или reject. В функции должна присутствовать как минимум 1 асинхронная операция
- Добавляем с помощью методов then/catch функции – обработчики результата.
Генераторы. Yield
Также в стандарте ES6 был определен новый вид функций – генераторы. Эти функции имеют возможность при идентичных на первый взгляд вызовах несколько раз возвращать разное значение. Давайте разберемся, как они это делают и зачем этим пользоваться.
Стандартный вид генератора: function* functionName() {}. В теле самих функций для возвращения промежуточного значения используется слово yield.
В качестве примера рассмотрим следующий генератор:
function* generateNumber() {
yield 1;
yield 2;
return 3;
}
В данный момент генератор находится в начале своего выполнения. При каждом вызове метода генератора next будет выполнен код, описанный до ближайшего yield (или return), а так же будет возвращено значение, указанное в строке с одним из этих слов.
Let one = generateNumber.next(); // {value: 1, done: false}
Следующий вызов аналогичным образом вернет значение 2. Третий вызов вернет 3 значение, и закончит исполнение функции.
Let two = generateNumber.next(); // {value: 2, done: false}
Let three = generateNumber.next(); // {value: 3, done: false}
Несмотря на это, к генератору все еще можно будет обратиться через функцию next. Он, впрочем, будет возвращать одно и то же значение: объект {done: true}.
ES7. Async/await
Вместе со стремлением угодить любителям ООП с помощью синтаксического сахара классов и имитации наследований, создатели ES7 пытаются облегчить понимание javascript и для любителей писать синхронный код. С помощью конструкций async/await пользователь имеет возможность писать асинхронный код максимально похожий на синхронный. При желании можно избавиться от недавно изученных промисов и переписать код с минимальными изменениями.
Рассмотрим пример:
Используя промисы:
requestBook(id) {
return bookAPIHelper.getBook(id).then(book => {console.log(book)});
}
С помощью async/await.
async requestBook(id) {
Const book = await bookAPIHelper.getBook(id);
Console.log(book);
}
Давайте опишем увиденное:
1) Async – ключевое слово, добавляемое при объявлении асинхронной функции
2) Await – ключевое слово, добавляемое при вызове асинхронной функции.
ES8. Асинхронная Итерация
Синхронно итерироваться по данным стало возможно еще в ES5. Спустя две спецификации было решено добавить возможность асинхронной итерации, работающей в асинхронных источниках данных. Теперь при вызове next() возвращаться будет не {value, done}, а промис (см. ES6).
Давайте рассмотрим функцию createAsyncIterable(iterable).
async function* createAsyncIterable(iterable) {
for (const elem of iterable) {
yield elem;
}
}
Как видим, функция инициализирует коллекцию, для каждого обращения к элементам которой будет возвращен промис со значением, указанным в iterable.
const asyncIterable = createAsyncIterable(['async 1', 'async 2']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator.next()
.then(result => {
console.log(result);
// {
// value: 'async 1',
// done: false,
// }
return asyncIterator.next();
})
.then(result => {
console.log(result);
// {
// value: 'async 2',
// done: false,
// }
return asyncIterator.next();
})
.then(result => {
console.log(result);
// {
// value: 'undefined',
// done: true,
// }
});
Более того, в новом стандарте был определен удобный для подобных операций цикл for-await-of.
for await (const x of createAsyncIterable(['a', 'b']))
TL;DR
Вовсе не обязательно знать и наизусть помнить к какой версии ECMAScript относится тот или иной синтаксис, особенно если вы только начали своё знакомство с асинхронным поведением в JS. В то же время, изучение асинхронности именно в порядке, предлагаемом историей развития спецификаций позволит программисту не только в совершенстве понять синтаксис и инструкции, передаваемые JS движку, но и проследить логику совершенствования ECMAScript как продукта, понять тенденции, диктуемые JS разработчиками, разделить их и принять.
Если коротко, то:
Callbacks <= ES5
Promises, Yield (Генераторы): ES6
Async/await: ES7
Async Iterators: ES8