JavaScript async / await: Преимущества, возможные сложности и способы использования

JavaScript async / await: Преимущества, возможные сложности и способы использования

От автора: Async await JavaScript, представленный в ES7, является фантастическим улучшением асинхронного программирования. Он предоставил возможность использовать код в синхронном стиле для доступа к ресурсам асинхронно, без блокировки основного потока. Однако этот способ немного сложен в использовании. В этой статье мы рассмотрим async / wait с разных точек зрения и покажем, как использовать их правильно и эффективно.

Преимущества async / wait

Важнейшим преимуществом async / wait является синхронный стиль программирования. Давайте рассмотрим пример.

// async/await
async getBooksByAuthorWithAwait(authorId) { const books = await bookModel.fetchAll(); return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) { return bookModel.fetchAll() .then(books => books.filter(b => b.authorId === authorId));
}

Очевидно, что async / await версия более проста для понимания, чем код с promise. Если вы игнорируете ключевое слово await, код будет выглядеть как любые другие синхронные языки, такие как Python.

И лучшее — это не только лучше читаемо, async / await имеет встроенную поддержку браузеров. На сегодняшний день все основные браузеры полностью поддерживают асинхронные функции.

Все основные браузеры поддерживают асинхронные функции

Встроенная поддержка означает, что вам не нужно транспилировать код. Что еще более важно, это облегчает отладку. Когда вы устанавливаете контрольную точку в точке входа функции и переходите через строку await, вы увидите прерывание отладчика, в то время как bookModel.fetchAll() выполняет свою работу, а затем переходит к следующей строке .filter! Это намного проще, чем с promise, для которого вам нужно настроить другую контрольную точку в строке .filter.

Отладка асинхронной функции. Отладчик будет ожидать строки await и перейдет дальше после обработки

Еще одним очевидным преимуществом является ключевое слово async. Оно объявляет, что возвращаемое значение функции getBooksByAuthorWithAwait() гарантировано является promise, так что вызывающие объекты могут безопасно вызывать getBooksByAuthorWithAwait().then(…) или await getBooksByAuthorWithAwait(). Рассмотрим следующий случай (плохая практика!):

getBooksByAuthorWithPromise(authorId) { if (!authorId) { return null; } return bookModel.fetchAll() .then(books => books.filter(b => b.authorId === authorId));
}

В этом коде getBooksByAuthorWithPromise может вернуть promise (нормальный случай) или нулевое значение (исключение), и в этом случае вызывающий объект не может безопасно вызвать .then(). При объявлении async это невозможно для такого кода.

Async / wait могут ввести в заблуждение

Некоторые статьи сравнивают async / await с Promise и утверждают, что это следующее поколение в эволюции асинхронного программирования JavaScript, с чем я — при всем уважении — не согласен. Async / await — это улучшение, но это не более чем синтаксический прием, который полностью не изменит наш стиль программирования.

По сути, асинхронные функции по-прежнему являются Promise. Вы должны понимать Promise, прежде чем сможете использовать асинхронные функции правильно, и что еще хуже, большую часть времени вы должны использовать Promise вместе с асинхронными функциями.

Рассмотрим функции getBooksByAuthorWithAwait() и getBooksByAuthorWithPromises() из приведенного выше примера. Обратите внимание, что они не только идентичны функционально, но и имеют точно такой же интерфейс!

Это означает, что getBooksByAuthorWithAwait() вернет promise, если вы его вызовете напрямую. Ну, это не обязательно плохо. Только слово await дает людям ощущение: «О, отлично, это может преобразовать асинхронные функции в синхронные», что на самом деле неверно.

Сложности с await / await

Итак, какие ошибки могут быть допущены при использовании async / wait? Вот некоторые из них.

Не будьте слишком последовательными

Хотя wait может сделать ваш код выглядящим синхронным, имейте в виду, что он все еще асинхронный, и нужно быть осторожными, чтобы не быть слишком последовательными.

async getBooksAndAuthor(authorId) { const books = await bookModel.fetchAll(); const author = await authorModel.fetch(authorId); return { author, books: books.filter(book => book.authorId === authorId), };
}

Этот код выглядит логически корректным. Однако это неверно.

await bookModel.fetchAll() будет ждать, пока не будет возвращена fetchAll().

После этого будет вызвана await authorModel.fetch(authorId).

Обратите внимание, что authorModel.fetch(authorId) не зависит от результата bookModel.fetchAll(), и на самом деле их можно вызывать параллельно! Однако, используя await мы делаем эти два вызова последовательными, и общее время выполнения будет намного больше, чем при параллельном выполнении. Вот корректный способ:

async getBooksAndAuthor(authorId) { const bookPromise = bookModel.fetchAll(); const authorPromise = authorModel.fetch(authorId); const book = await bookPromise; const author = await authorPromise; return { author, books: books.filter(book => book.authorId === authorId), };
}

Или еще хуже, если вы хотите получить список элементов один за другим, вы должны полагаться на promises:

async getAuthors(authorIds) { // НЕВЕРНО, это приведет к последовательным вызовам // const authors = _.map( // authorIds, // id => await authorModel.fetch(id));
// ВЕРНО const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises);
}

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

Обработка ошибок

При использовании promises асинхронная функция может возвращать два значения: обработанное значение и отклоненное значение. И мы можем использовать .then() для стандартного случая и .catch() для исключения. Однако при использовании async/await обработка ошибок может быть более сложной.

try…catch

Стандартный (и рекомендуемый мой) способ — использовать инструкцию try … catch. При вызове await любое отклоненное значение будет введено как исключение. Вот пример:

class BookModel { fetchAll() { return new Promise((resolve, reject) => { window.setTimeout(() => { reject({'error': 400}) }, 1000); }); }
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try { const books = await bookModel.fetchAll();
} catch (error) { console.log(error); // { "error": 400 }
}

Перехваченная ошибка — это собственно отклоненное значение. После того, как мы перехватили исключение, у нас есть несколько способов обработать его:

Обработать исключение и вернуть нормальное значение. (Неиспользование оператора return в блоке catch эквивалентно использованию return undefined и также является нормальным значением.)

Ввести его, если вы хотите, чтобы вызывающий объект обработал его. Вы можете либо напрямую ввести простой объект ошибки, например, throw error;, который позволяет использовать функцию async getBooksByAuthorWithAwait() в цепочке promises (т. е. вы все равно можете называть ее как getBooksByAuthorWithAwait().then(…).catch(error => …)); или вы можете обернуть ошибку в объект Error, например, throw new Error(error), которая даст полную трассировку стека, когда эта ошибка будет отображаться в консоли.

Отклонить его, например, return Promise.reject(error). Это эквивалентно throw error, поэтому не рекомендуется.

Преимущества использования try … catch:

Простой, традиционный способ. Если у вас есть опыт работы с другими языками, такими как Java или C++, вам не составит труда разобраться в нем.

Вы можете обернуть несколько вызовов в await один блок try … catch для обработки ошибок в одном месте, если обработка ошибок на каждом шаге не требуется.

У этого подхода есть и один недостаток. Так как try … catch перехватывает каждое исключение в блоке, будут перехвачены некоторые исключения, которые обычно не перехватываются для promises. Рассмотрим следующий пример:

class BookModel { fetchAll() { cb(); // note `cb` is undefined and will result an exception return fetch('/books'); }
}
try { bookModel.fetchAll();
} catch(error) { console.log(error); // This will print "cb is not defined"
}

Запустите этот код и вы получите ошибку ReferenceError: cb is not defined в консоли, черного цвета. Ошибка выводилась с помощью console.log(), но не самим JavaScript. Иногда это может быть критически: если BookModel заключен глубоко в ряд вызовов функций, и один из вызовов выдает ошибку, будет очень сложно найти ошибку, подобную этой.

Возврат функциями обоих значений

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

 [err, user] = await to(UserModel.findById(1));

Лично мне не нравится этот подход, поскольку он привносит в JavaScript стиль Go, который кажется неестественным, но в некоторых случаях это может оказаться весьма полезным.

Использование .catch

Последний подход, который мы представим здесь — продолжить использование .catch(). Вспомните функционал await: она будет ждать, пока promise завершит работу. Также, пожалуйста, не забывайте, что prom.catch() возвращает promise ! Поэтому мы можем задать обработку ошибок следующим образом:

// books === undefined если происходит ошибка,
// так как ничего не возвращается в операторе catch
let books = await bookModel.fetchAll() .catch((error) => { console.log(error); });

В этом подходе есть две незначительные проблемы:

Это смесь promises и асинхронных функций. Вы все равно должны понимать, как работают promises, чтобы читать его.

Обработка ошибок происходит до нормального сценария, что не является интуитивной.

Заключение

Ключевые слова async/await, введенные ES7, безусловно, являются улучшением асинхронного программирования JavaScript. Это может сделать код более простым для чтения и отладки. Однако, чтобы правильно использовать их, нужно полностью понимать promises, так как они не более чем синтаксический прием, а основной метод по-прежнему promises.

Надеюсь, что этот пост помог вам получить лучшее представление о том, что такое async/await, и это поможет вам не допускать некоторые распространенные ошибки. Спасибо за прочтение, и, пожалуйста, похлопайте мне, если вам понравился этот пост.

Автор: Charlee Li

Источник: https://hackernoon.com/

Редакция: Команда webformyself.