От автора: возможность писать на JavaScript асинхронные функции является важным обновлением в ES2017.
Что такое асинхронные функции?
Асинхронные функции — это функции, которые возвращают promise. Мы обозначаем их, используя ключевое слово async.
1 2 3 4 5 6 7 8 9 10 11 |
const loadData = async function( value ) { if ( value > 0 ) { return { data: value }; } else { throw new Error( 'Value must be greater than 0' ); } } loadData( 1 ).then( response => console.log( response ) ); loadData( 0 ).catch( error => console.log( error ) ); |
Когда loadData возвращает объект, возвращаемое значение оборачивается в promise. По мере того, как это promise обрабатывается, выполняется обратный вызов then и консоль регистрирует ответ.
Когда loadData вызывается с аргументом 0, возникает ошибка. Эта ошибка оборачивается в отклоненный promise, который обрабатывается обратным вызовом catch.
В общем случае возвращаемые значения функции async оборачиваются в обработанный вызов promise, за исключением тех случаев, когда возвращаемое значение является promise. Тогда возвращается promise.
Ошибки, введенные в асинхронную функцию, вырезаются и оборачиваются в отклоненный promise.
Оператор await
Await — это префиксный оператор, который указывается перед promise. Пока promise после оператора await находится в состоянии ожидания, promise блокирует выполнение.
После того, как promise будет обработан, await возвращает значение исполнения promise. Если promise будет отклонен, await сбрасывает отклоненное значение. Давайте рассмотрим пример:
1 2 3 4 5 6 7 8 9 10 11 12 |
const delayedPromise = async () => { let p = new Promise( ( resolve, reject ) => { setTimeout( () => resolve( 'done' ), 1000 ); } ); const promiseValue = await p; console.log( 'Promise value: ', promiseValue ); } delayedPromise(); // ... через 1 секунду > Promise value: done |
Оператор await может использоваться только в асинхронных функциях. Если из предыдущего примера удалить ключевое слово async, возникает ошибка:
1 2 3 4 5 6 7 8 9 |
const delayedPromise2 = () => { let p = new Promise( ( resolve, reject ) => { setTimeout( () => resolve( 'done' ), 1000 ); } ); const promiseValue = await p; console.log( 'Promise value: ', promiseValue ); } > Uncaught SyntaxError: Unexpected identifier |
Сочетание async и await
Мы уже знаем, что асинхронные функции возвращают promise. Мы также знаем, что ключевое слово await:
выполняет promise в качестве своего операнда,
должно использоваться в асинхронных функциях.
Как следствие, мы можем ожидать ответа асинхронных функций внутри асинхронных функций.
1 2 3 4 5 6 7 8 |
const loadData = async () => { disableSave(); const resultSet1 = await asyncQuery1(); displayResultSet1( resultSet1 ); const resultSet2 = await asyncQuery2(); displayResultSet2( resultSet2 ); enableSave(); } |
Эта гипотетическая функция loadData загружает две таблицы, обращаясь к серверу через API. Выполняется первый запрос 1. Выполнение loadData блокируется до тех пор, пока не будет обработан promise, возвращаемый asyncQuery1. После того, как станет доступен resultSet1, выполняется функция displayResultSet1.
После этого выполняется asyncQuery2. Обратите внимание, что эта функция вызывается только после обработки возвращаемого значения asyncQuery1. Другими словами, asyncQuery1 и asyncQuery2 выполняются синхронно. После того, как resultSet2 становится доступным, отображаются результаты.
В этом примере есть только одна проблема. Представьте, что веб-приложение имеет доступ к десяти конечным точкам API. Предположим, что каждый вызов сервера занимает в среднем одну секунду. Если наша страница может быть отображена только после того, как будут выполнены все десять асинхронных вызовов, нам придется ждать десять секунд, пока пользователь сможет просмотреть страницу. Это неприемлемо.
Вот почему имеет смысл выполнять асинхронные запросы параллельно. Мы можем использовать Promise.all для создания promise, который объединяет и выполняет свои аргументы параллельно.
1 2 3 4 5 6 7 8 9 10 |
const loadData = async () => { disableSave(); const [resultSet1, resultSet2] = await Promise.all([ asyncQuery1(), asyncQuery2() ] ); displayResultSet1( resultSet1 ); displayResultSet2( resultSet2 ); enableSave(); } |
В этом примере все запросы выполняются асинхронно. Если массив внутри Promise.all содержал десять запросов, и выполнение каждого запроса занимает одну секунду, время выполнения всего выражения Promise.all все равно составит одну секунду.
Однако эти два решения не эквивалентны. Предположим, что среднее время, затраченное на извлечение resultSet1, составляет 0,1 секунды, а на извлечение resultSet2 требуется одна секунда. В этом случае:
асинхронная версия экономит 0,1 секунды по сравнению с синхронной,
однако displayResultSet1 выполняется только после того, как все запросы будут получены в асинхронной версии. Это означает, что мы можем ожидать на 0,9 секунды больше по сравнению с синхронной версией.
Мы можем объединить преимущества двух версий, используя цепочку последовательных обратных вызовов then promise.
1 2 3 4 5 6 7 8 |
const loadData = async () => { disableSave(); const [resultSet1, resultSet2] = await Promise.all([ asyncQuery1().then( displayResultSet1 ), asyncQuery2().then( displayResultSet2 ) ] ); enableSave(); } |
В этой версии кода запросы выполняются асинхронно, и соответствующая функция обработчика displayResultSet выполняется, как только соответствующий promise будет обработан. Это означает, что первый запрос отображается за 0,1 секунды, а второй — за одну секунду.
Параллельное выполнение без await
Удалим функции disableSave и enableSave из предыдущего примера:
1 2 3 4 5 6 |
const loadData = async () => { const [resultSet1, resultSet2] = await Promise.all([ asyncQuery1().then( displayResultSet1 ), asyncQuery2().then( displayResultSet2 ) ] ); } |
Функция по-прежнему работает, как ожидалось, однако реализация становится сложной без каких-либо причин.
Мы могли бы просто выполнить два асинхронных запроса и их соответствующие обработчики один за другим, не оборачивая их в Promise.all:
1 2 3 4 |
const loadData = () => { asyncQuery1().then( displayResultSet1 ); asyncQuery2().then( displayResultSet2 ); } |
Не используя await, мы не блокируем выполнение asyncQuery2 до того, как будет обработан promise asyncQuery1. Поэтому два запроса все равно выполняются параллельно. Обратите внимание, что эта реализация loadData даже не объявлена как async, так как нам не нужно возвращать обещание promise, и мы больше не используем ключевое слово await в функции.
Ожидание отклоненного promise
Бывают случаи, когда операнд await становится отклоненным promise. Например:
при чтении файла, который не существует,
при ошибке ввода-вывода,
при таймауте сеанса в случае вызова API,
наш promise будет отклонен.
Когда promise p отклоняется, await p, выдает ошибку. Как следствие, мы должны обрабатывать все источники ошибок путем размещения выражений, ожидающих появления ошибок, в блоках try-catch.
1 2 3 4 5 |
try { await p; } catch( e ) { /* обработка ошибки */ } |
Позиция ключевого слова async
Во-первых, мы можем создавать именованные выражения асинхронных регулярных функций или функций стрелок.
1 2 3 |
const name1 = async function() { ... } const name2 = ( ...args ) => returnValue; |
При создании выражений функций async указывает перед ключевым словом function.
1 |
async function name3() { ... } |
Заключение
Асинхронные функции — это функции, которые возвращают promise. Эти функции могут обрабатывать операции ввода-вывода, вызовы API и другие формы отложенного выполнения. Ожидание обработки promise возвращает обработанное значение promise или выдает ошибку при отклонении. Оператор await позволяет выполнять асинхронные функции последовательно или параллельно. Async-await дает вам элегантный способ обработки асинхронных функций, и, следовательно, это одно из самых полезных обновлений ES2017.
Источник: //www.zsoltnagy.eu/a>
Редакция: Команда webformyself.