Синхронный против асинхронного
Сперва давайте рассмотрим два термина — синхронный и асинхронный.
По умолчанию JavaScript является синхронным однопоточным языком программирования. Это означает, что инструкции могут выполняться только одна за другой, а не параллельно. Рассмотрим небольшой фрагмент кода ниже:
1 2 3 4 |
let a = 1; let b = 2; let sum = a + b; console.log(sum); |
Приведенный выше код довольно прост — он суммирует два числа, а затем записывает сумму в консоль браузера. Интерпретатор выполняет эти инструкции одну за другой в указанном порядке, пока не будет выполнено.
Но этот метод сопряжен с недостатками. Скажем, мы хотели получить большое количество данных из базы данных, а затем отобразить их в нашем интерфейсе. Когда интерпретатор достигает инструкции, которая извлекает эти данные, выполнение остальной части кода блокируется до тех пор, пока данные не будут извлечены и возвращены.
Но вы можете сказать, что данные, которые нужно извлечь, не так уж велики, и это не займет заметного времени. Представьте, что вам нужно получить данные из нескольких таблиц и еще сделать преобразования над данными. Эта усугубленная задержка не похожа на то, с чем пользователи хотели бы столкнуться.
К счастью для нас, проблемы с синхронным JavaScript были решены путем введения асинхронного JavaScript.
Думайте об асинхронном коде как о коде, выполнение которого может начаться сейчас и закончить его чуть позже. Когда JavaScript работает асинхронно, инструкции не обязательно выполняются одна за другой, как мы видели ранее.
Чтобы правильно реализовать это асинхронное поведение, разработчики использовали несколько различных решений на протяжении многих лет. Каждое решение улучшает предыдущее, что делает код более оптимизированным и понятным, если он усложняется.
Чтобы лучше понять асинхронную природу JavaScript, мы рассмотрим функции обратного вызова (callback functions), промисы (promises), асинхронность (async) и ожидание (await).
Что такое обратные вызовы в JavaScript?
Callback (функция обратного вызова) — это функция, которая передается внутри другой функции, а затем вызывается в этой функции для выполнения задачи.
Сбивает с толку? 🙂 Давайте разберем его на практике.
1 2 3 4 5 6 7 8 |
console.log('fired first'); console.log('fired second'); setTimeout(()=>{ console.log('fired third'); },2000); console.log('fired last'); |
Приведенный выше фрагмент представляет собой небольшую программу, которая выводит данные в консоли. Но здесь есть что-то новое. Интерпретатор выполнит первую инструкцию, затем вторую, но пропустит третью и выполнит последнюю.
setTimeout
— это функция JavaScript, которая принимает два параметра.
- Первый параметр — это другая функция,
- а второй — время, по истечении которого эта функция должна выполняться в миллисекундах.
Теперь вы видите определение обратных вызовов, вступающих в игру.
Функция внутри setTimeout
в этом случае должна запускаться через две секунды (2000 миллисекунд). Представьте, что она переносится для выполнения в какую-то отдельную часть браузера, в то время как другие инструкции продолжают выполняться. Через две секунды возвращаются результаты функции.
Вот почему, если мы запустим приведенный выше фрагмент в нашей программе, мы получим это:
1 2 3 4 |
fired first fired second fired last fired third |
Вы видите, что последняя инструкция регистрируется до того, как функция setTimeout
возвращает свой результат. Скажем, мы использовали этот метод для извлечения данных из базы данных. Пока пользователь ожидает возврата результатов вызовом базы данных, выполнение потока не будет прервано.
Этот метод был очень эффективным, но только до определенного момента. Иногда разработчикам приходится делать несколько обращений к разным источникам в своем коде. Чтобы сделать эти вызовы, обратные вызовы вкладывают друг в друга до тех пор, пока их не станет очень трудно читать или поддерживать. Это называется Callback Hell
Чтобы исправить эту проблему, были введены обещания (promises).
Что такое обещания (promises) в JavaScript?
Мы постоянно слышим, как люди дают обещания. Твой двоюродный брат, который обещал прислать тебе деньги, ребенок, обещающий больше не трогать банку с печеньем без разрешения… но обещания в JavaScript немного отличаются.
Promise в нашем контексте — это то, что требует некоторого времени для выполнения. Есть два возможных результата обещания:
- Мы либо запускаем и разрешаем (resolve) promise, либо
- В строке происходит какая-то ошибка, и обещание отклоняется (reject)
Появились promises, чтобы решить проблемы функций обратного вызова.
Promise принимает две функции в качестве параметров:
- resolve() — это успех.
- reject() — при возникновении ошибки.
Давайте посмотрим на обещания в действии:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const getData = (dataEndpoint) => { return new Promise ((resolve, reject) => { //some request to the endpoint; if(request is successful){ //do something; resolve(); } else if(there is an error){ reject(); } }); }; |
Приведенный выше код представляет собой promise, заключенный в запрос к некоторому endpoint. Обещание вступает в силу в момент выполнения resolve()
или reject()
.
Например, после вызова endpoint, если запрос будет успешным, мы разрешим обещание и продолжим делать с ответом все, что захотим. Но если есть ошибка, обещание будет отклонено.
Промисы — это удобный способ исправить проблемы, вызванные адом обратных вызовов, в методе, известном как цепочка промисов. Вы можете использовать этот метод для последовательного получения данных из нескольких конечных точек, но с меньшим количеством кода и более простыми методами.
Но есть еще лучший способ! Возможно, вам знаком следующий метод, так как это предпочтительный способ обработки данных и вызовов API в JavaScript.
Что такое Async и Await в JavaScript?
Дело в том, что объединение обещаний вместе, как и обратных вызовов, может стать довольно громоздким и запутанным.
Вот почему были созданы Async и Await.
Чтобы определить асинхронную функцию, вы делаете следующее:
1 2 3 |
const asyncFunc = async() => { } |
Обратите внимание, что вызов асинхронной функции всегда возвращает обещание. Взгляните на это:
1 2 |
const test = asyncFunc(); console.log(test); |
Запустив приведенное выше в консоли браузера, мы видим, что asyncFunc
возвращает обещание.
Давайте действительно разберем код. Рассмотрим небольшой фрагмент ниже:
1 2 3 4 |
const asyncFunc = async () => { const response = await fetch(resource); const data = await response.json(); } |
Ключевое слово
async — это то, что мы используем для определения асинхронных функций.
Но как насчет
await? Await останавливает JavaScript от назначения fetch переменной ответа до тех пор, пока обещание не будет разрешено. Как только обещание было разрешено, результаты метода выборки теперь могут быть назначены переменной ответа.
То же самое происходит в строке 3: Метод json
возвращает обещание и мы можем использовать await
все еще, чтобы отложить назначение до тех пор, пока обещание не будет разрешено.
Блокировать код или не блокировать код
Когда говорится про «зависание», вы должны думать, что реализация Async
и Await
каким-то образом блокирует выполнение кода. Потому что что, если наш запрос займет слишком много времени?
Факт в том, что это не так. Код внутри асинхронной функции является блокирующим, но это никак не влияет на выполнение программы. Выполнение нашего кода такое же асинхронное, как и всегда. Чтобы показать это, рассмотрим следующий пример:
1 2 3 4 5 6 7 8 9 10 11 12 |
const asyncFunc = async () => { const response = await fetch(resource); const data = await response.json(); } console.log(1); cosole.log(2); asyncFunc().then(data => console.log(data)); console.log(3); console.log(4); |
В консоли нашего браузера вывод приведенного выше кода будет выглядеть примерно так:
1 2 3 4 5 |
1 2 3 4 data returned by asyncFunc |
Вы видите, что пока мы вызывали asyncFunc, наш код продолжал работать до тех пор, пока функция не возвращала результаты.
Async функции
Обычные функции возвращают результат с помощью ключевого слова return:
1 2 3 4 5 |
const fn = () => { return 5; } fn(); // 5 |
Асинхронные функции, напротив, возвращают Promise:
1 2 3 4 5 |
const asyncFn = async () => { return 5; } asyncFn(); // Promise |
Вы можете сделать любую функцию асинхронной с помощью ключевого слова async, и существует много функций, которые возвращают Promise вместо результата.
Например, чтобы прочитать файл из файловой системы, вы можете использовать fs.promises, вариант функций fs, возвращающих промисы:
1 2 3 4 |
const fs = require("fs"); fs.promises.readFile("test.js"); // Promise |
Или конвертируйте изображение в jpeg с помощью библиотеки Sharp, которая также возвращает Promise:
1 2 3 4 5 6 |
const sharp = require("sharp"); sharp("image.png") .jpeg() .toFile("image.jpg"); // Promise |
Или сделайте сетевой запрос с помощью fetch:
1 2 |
fetch("https://yandex.com"); // Promise |
Как использовать Promise
Асинхронная функция по-прежнему имеет return значение, и обещание содержит этот результат. Чтобы получить доступ к значению, прикрепите обратный вызов с помощью функции then(). Этот обратный вызов будет вызван с результатом функции.
Рассмотрим пример получения содержимого файла после readFile:
1 2 3 4 5 6 |
const fs = require("fs"); fs.promises.readFile("test.js").then((result) => { console.log(result); // Buffer }); |
Точно так же, чтобы получить результат нашей простой асинхронной функции, используйте then():
1 2 3 4 5 6 7 8 |
const asyncFn = async () => { return 5; } asyncFn().then((res) => { console.log(res); // 5 }); |
Преимущества Promises
Но зачем усложнять вызов функции промисами? Обычная функция просто возвращает значение, которое можно использовать в следующей строке, без каких-либо обратных вызовов.
Преимущество возврата обещания вместо значения заключается в том, что результат может быть не готов к моменту возврата функции (к моменту выполнения return). Обратный вызов может быть вызван намного позже, но функция должна вернуться немедленно. Это расширяет возможности функции.
1 2 3 4 5 6 7 8 9 |
// sync fn(); // result is ready // async asyncFn().then(() => { // result is ready }) // result is pending |
Во многих случаях синхронный результат невозможен. Например, выполнение сетевого запроса занимает целую вечность по сравнению с вызовом функции. С промисами не имеет значения, занимает ли что-то много времени или дает результат сразу. В обоих случаях функция then() будет вызвана, когда результат будет готов.
Схема «Асинхронная функция возвращает обещание»
Использование стандартизированных промисов также позволяет создавать на их основе другие конструкции. Как мы видели, ключевое слово async заставляет функцию возвращать Promise вместо значения.
Резюме
Асинхронные функции возвращают обещания, которые являются значениями, доступными когда-нибудь в будущем.
Ключевое слово await
Using Promises with callbacks requires changes to the code structure and it makes the code harder to read.
Instead of a flat structure of synchronous function calls:
Использование Promises с обратными вызовами (callbacks) требует изменений в структуре кода и затрудняет чтение кода.
Вместо плоской структуры синхронных вызовов функций:
1 2 3 4 5 6 |
const user = getUser(); const permissions = getPermissions(); const hasAccess = checkPermissions(user, permissions); if (hasAccess) { // handle request } |
Для Promises необходимо вызывать callbacks:
1 2 3 4 5 6 7 8 |
getUser().then((user) => { getPermissions().then((permissions) => { const hasAccess = checkPermissions(user, permissions); if (hasAccess) { // handle request } }); }); |
Примечание
Promises поддерживают плоскую структуру, когда они вызываются последовательно, это их главное преимущество перед традиционными обратными вызовами. Например, ряд асинхронных вызовов может обрабатывать объект:
1 2 3 4 5 6 |
getUser() .then(getPermissionsForUser) .then(checkPermission) .then((allowed) => { // handle allowed or not }); |
Это почти плоская структура, но в конце все еще необходим обратный вызов. Ключевое слово await устраняет необходимость в этом.
Чтобы решить эту проблему, не теряя преимуществ промисов, асинхронные функции могут использовать ключевое слово await. Он останавливает функцию до тех пор, пока обещание не будет готово, и возвращает значение результата.
1 2 3 4 5 6 |
const asyncFn = async () => { return 5; } await asyncFn(); // 5 |
Приведенный выше код, использующий обратные вызовы, может вместо этого использовать await, что приводит к более знакомой структуре:
1 2 3 4 5 6 |
const user = await getUser(); const permissions = await getPermissions(); const hasAccess = checkPermissions(user, permissions); if (hasAccess) { // handle request } |
Ключевое слово await, ожидающее асинхронных результатов, делает код почти синхронным, но со всеми преимуществами Promises.
Обратите внимание, что await останавливает выполнение функции, что кажется невозможным в Javascript. Но под капотом он по-прежнему использует обратные вызовы then(), и, поскольку асинхронные функции возвращают промисы, им не нужно немедленно предоставлять результат. Это позволяет остановить функцию без существенных изменений в том, как работает язык.
Примеры Async функции
Асинхронные функции (Async function) с await являются мощным инструментом. Они делают сложный и асинхронный рабочий процесс простым и знакомым, скрывая все сложности результатов, получаемых позже.
Автоматизация браузера является ярким примером. Проект Puppeteer позволяет запускать Chromium и управлять им с помощью протокола DevTools.
Сделать скриншот веб-страницы — это всего несколько строк:
1 2 3 4 5 |
const browser = await puppeteer.launch(options); const page = await browser.newPage(); const response = await page.goto(url); const img = await page.screenshot(); await browser.close(); |
Этот код скрывает большую сложность. Он запускает браузер, затем отправляет ему команды, все асинхронно, поскольку браузер — это отдельный процесс.
Но конечным результатом является буфер изображения, содержащий снимок экрана.
Примечание
Приведенный выше код оставляет браузер запущенным, если во время выполнения возникает ошибка.
Другой пример — взаимодействие с базами данных. Удаленной службе всегда требуются сетевые вызовы, а это означает асинхронные результаты.
Этот код, используемый в функции AWS Lambda, обновляет изображение аватара пользователя:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// move object out of the pending directory await s3.copyObject({/*...*/}).promise(); await s3.deleteObject({/*...*/}).promise(); // get the current avatar image const oldAvatar = (await dynamodb.getItem({/*...*/}).promise()) .Item.Avatar.S; // update the user's avatar await dynamodb.updateItem({/*...*/}).promise(); // delete the old image await s3.deleteObject({/*...*/}).promise(); return { statusCode: 200, }; |
Он обращается к сервису AWS S3 для перемещения объектов и к базе данных DynamoDB для чтения и изменения данных. Оба они являются удаленными сервисами, но все сложности скрыты за await.
Резюме
Ключевое слово await останавливает функцию до тех пор, пока не станет доступен будущий результат.
- Основная суть async/await: выполнять асинхронные операции синхронным образом.
- Ключевое слово await можно использовать только в асинхронных функциях, иначе будет выдано сообщение об ошибке.
Chaining Promises (Цепочка обещаний)
Мы видели, что когда асинхронная функция возвращает значение, оно будет заключено в Promise, а ключевое слово await извлекает из него значение. Но что происходит, когда асинхронная функция возвращает обещание? Означает ли это, что вам нужно использовать два await?
Рассмотрим следующий код:
1 2 3 4 5 6 7 8 9 |
const f1 = async () => { return 2; }; const f2 = async () => { return f1(); } const result = await f2(); |
Строго следуя процессу, f1() возвращает Promise<2>, а f2() возвращает Promise<Promise<2>>, поэтому значением результата будет Promise<2> вместо 2.
Но это не то, что происходит. Когда асинхронная функция возвращает обещание, она возвращает его без добавления другого слоя. Не имеет значения, возвращает ли он значение или промис, это всегда будет промис, и он всегда будет разрешаться с окончательным значением, а не с другим промисом.
То же самое работает и для цепочек Promises. Обратный вызов .then() также заключен в Promise, если его еще нет, поэтому вы можете легко связать их в цепочку:
1 2 3 4 5 6 7 8 9 10 11 12 |
getUser() .then(async function getPermissionsForUser(user) { const permissions = await // ...; return permissions; }) .then(async function checkPermission(permissions) { const allowed = await // ...; return allowed; }) .then((allowed) => { // handle allowed or not }); |
Функция getUser возвращает Promise<User>, затем getPermissionsForUser получает объект пользователя (разрешенное значение), а затем возвращает набор permission.
Следующий вызов checkPermission получает permissions, и т.д.
Полезным аналогом является то, как работает функция flatMap для массива. Неважно, возвращает он значение или массив, конечным результатом всегда будет массив со значениями. Это map, за которой следует flat.
1 2 3 4 5 |
[1].flatMap((a) => 5) // [5] [1].flatMap((a) => [5]) // [5] [1].flat() // [1] [[1]].flat() // [1] |
Когда я не уверен, что возвращает цепочка Promises, я мысленно перевожу промисы в массивы, где каждая асинхронная функция возвращает плоский массив со своим результатом, а await получает первый элемент:
1 2 3 4 5 6 7 8 9 |
const f1 = () => { return [2].flat(); }; const f2 = () => { return [f1()].flat(); } const result = f2()[0]; // 2 |
Затем цепочка Promises становится серией flatMap:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const getUser = () => ["user"]; getUser() .flatMap(function getPermissionsForUser(user) { // user = "user" const permissions = "permissions"; return permissions; }) .flatMap(function checkPermission(permissions) { // permissions = "permissions" const allowed = true; return allowed; }) .flatMap((allowed) => { // allowed = true // handle allowed or not }); |
Это устраняет большинство сложностей, связанных с асинхронностью, и об этом намного проще рассуждать.
Демонстрационные примеры
Использования Promise, Async, Await
Код примера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
function requestData1(){ return new Promise(resolve => { setTimeout(() => { let data = { "id":1, "name":'JavaScript', "skill":'Development' }; resolve(data); }, 2000) }) } function requestData2(){ return new Promise(resolve => { setTimeout(() => { let data = [1, 2, 3]; resolve(data); }, 2000) }) } async function getData () { console.log('start requestData1...'); const result1 = await requestData1(); console.log(result1); console.log('start requestData2...'); const result2 = await requestData2(); console.log(result2); } getData(); |
Результат выполнения:
1 2 3 4 |
start requestData1... { id: 1, name: 'JavaScript', skill: 'Development' } start requestData2... [ 1, 2, 3 ] |
Пример использования promise, await, async в цикле for loop
Код
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function requestData(num){ return new Promise(resolve => { setTimeout(() => { let data = [ {"id":1, "name":'Product 1', "skill":'Category 1'}, {"id":2, "name":'Product 2', "skill":'Category 2'} ]; resolve(data[num]); }, 2000) }) } function openSession(){ return new Promise(resolve => { setTimeout(() => { console.log('Session was opened'); resolve(); }, 2000) }) } const forLoop = async() => { let iterations = [0,1]; console.log('---Start---') for (let index = 0; index < iterations.length; index++) { await openSession(); const result = await requestData(index); console.log(result); } console.log('---End---') } forLoop(); |
Результат:
1 2 3 4 5 6 |
---Start--- Session was opened { id: 1, name: 'Product 1', skill: 'Category 1' } Session was opened { id: 2, name: 'Product 2', skill: 'Category 2' } ---End--- |
Демонстрация работы кода:
Leave a Reply