async/await
, представленный ES7, является фантастическим улучшением асинхронного программирования с помощью JavaScript. Он предоставил возможность использования кода синхронного стиля для асинхронного доступа к ресурсам без блокировки основного потока. Однако правильно его использовать немного сложно. В этой статье мы рассмотрим async / await с разных точек зрения и покажем, как их правильно и эффективно использовать.
Хорошая часть async / await
Самым важным преимуществом, которое async/await
принесло нам, является стиль синхронного программирования. Давайте посмотрим на пример.
// 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
version легче понять, чем обещанную версию. Если вы проигнорируете ключевое слово await
, код будет выглядеть как любой другой синхронный язык, такой как Python.
И золотая середина - это не только удобочитаемость. async/await
имеет встроенную поддержку браузера. На сегодняшний день все основные браузеры полностью поддерживают асинхронные функции.
Встроенная поддержка означает, что вам не нужно переносить код. Что еще более важно, это облегчает отладку. Если вы установите точку останова в точке входа функции и перейдете через строку await
, вы увидите, что отладчик остановится на короткое время, пока bookModel.fetchAll()
выполняет свою работу, а затем перейдет к следующей строке .filter
! Это намного проще, чем в случае с обещанием, в котором вам нужно установить другую точку останова в строке .filter
.
Еще одно менее очевидное преимущество - ключевое слово async
. Он заявляет, что возвращаемое значение функции getBooksByAuthorWithAwait()
гарантированно является обещанием, так что вызывающие абоненты могут безопасно вызывать getBooksByAuthorWithAwait().then(...)
или await getBooksByAuthorWithAwait()
. Подумайте об этом случае (плохая практика!):
getBooksByAuthorWithPromise(authorId) { if (!authorId) { return null; } return bookModel.fetchAll() .then(books => books.filter(b => b.authorId === authorId)); }
В приведенном выше коде getBooksByAuthorWithPromise
может возвращать обещание (нормальный случай) или значение null
(исключительный случай), и в этом случае вызывающая сторона не может безопасно вызвать .then()
. С объявлением async
это становится невозможным для такого кода.
Async / await может вводить в заблуждение
В некоторых статьях async / await сравнивается с Promise и утверждается, что это следующее поколение в эволюции асинхронного программирования JavaScript, с чем я с уважением не согласен. Async / await ЯВЛЯЕТСЯ улучшением, но это не более чем синтаксический сахар, который не изменит полностью наш стиль программирования.
По сути, асинхронные функции все еще остаются обещаниями. Вы должны понять обещания, прежде чем сможете правильно использовать асинхронные функции, и, что еще хуже, большую часть времени вам нужно использовать обещания вместе с асинхронными функциями.
Рассмотрим функции getBooksByAuthorWithAwait()
и getBooksByAuthorWithPromises()
в приведенном выше примере. Обратите внимание, что они не только идентичны функционально, но и имеют одинаковый интерфейс!
Это означает, что getBooksByAuthorWithAwait()
вернет обещание, если вы вызовете его напрямую.
Что ж, это не обязательно плохо. Только имя await
дает людям ощущение, что «О, здорово, это может преобразовывать асинхронные функции в синхронные функции», что на самом деле неверно.
Подводные камни async / await
Итак, какие ошибки могут быть сделаны при использовании async/await
? Вот несколько распространенных.
Слишком последовательно
Хотя await
может сделать ваш код похожим на синхронный, имейте в виду, что они по-прежнему асинхронны, и необходимо соблюдать осторожность, чтобы не быть слишком последовательным.
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), }; }
Или, что еще хуже, если вы хотите получить список элементов один за другим, вы должны полагаться на обещания:
async getAuthors(authorIds) { // WRONG, this will cause sequential calls // const authors = _.map( // authorIds, // id => await authorModel.fetch(id)); // CORRECT const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises); }
Короче говоря, вам все равно нужно думать о рабочих процессах асинхронно, а затем пытаться писать код синхронно с await
. В сложном рабочем процессе проще использовать обещания напрямую.
Обработка ошибок
С обещаниями асинхронная функция имеет два возможных возвращаемых значения: разрешенное значение и отклоненное значение. И мы можем использовать .then()
для нормального случая и .catch()
для исключительного случая. Однако с async/await
обработка ошибок может быть сложной.
попробуй поймать
Самый стандартный (и рекомендуемый мной) способ - использовать оператор 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 } }
Ошибка catch
ed - это в точности отклоненное значение. После того, как мы перехватили исключение, у нас есть несколько способов справиться с ним:
- Обработайте исключение и верните нормальное значение. (Отсутствие каких-либо операторов
return
в блокеcatch
эквивалентно использованиюreturn undefined;
и также является нормальным значением.) - Бросьте его, если вы хотите, чтобы вызывающий абонент справился с этим. Вы можете либо выбросить простой объект ошибки напрямую, как
throw error;
, что позволяет вам использовать этуasync getBooksByAuthorWithAwait()
функцию в цепочке обещаний (т.е. вы все равно можете называть ее какgetBooksByAuthorWithAwait().then(...).catch(error => ...)
); Или вы можете заключить ошибку в объектError
, напримерthrow new Error(error)
, который даст полную трассировку стека, когда эта ошибка отображается в консоли. - Отклоните это, например,
return Promise.reject(error)
. Это эквивалентthrow error
, поэтому не рекомендуется.
Преимущества использования try...catch
:
- Просто, традиционно. Если у вас есть опыт работы с другими языками, такими как Java или C ++, вам не составит труда понять это.
- Вы по-прежнему можете заключить несколько
await
вызовов в один блокtry...catch
для обработки ошибок в одном месте, если пошаговая обработка ошибок не требуется.
В этом подходе есть еще один недостаток. Поскольку try...catch
будет перехватывать каждое исключение в блоке, будут перехвачены некоторые другие исключения, которые обычно не перехватываются обещаниями. Подумайте об этом примере:
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));
Лично мне не нравится этот подход, поскольку он привносит стиль Go в JavaScript, что кажется неестественным, но в некоторых случаях это может быть весьма полезно.
Использование .catch
Последний подход, который мы здесь представим, - это продолжить использование .catch()
.
Вспомните функциональность await
: он будет ждать, пока обещание завершит свою работу. Также помните, что promise.catch()
тоже вернет обещание! Итак, мы можем написать такую обработку ошибок:
// books === undefined if error happens, // since nothing returned in the catch statement let books = await bookModel.fetchAll() .catch((error) => { console.log(error); });
В этом подходе есть две незначительные проблемы:
- Это смесь обещаний и асинхронных функций. Чтобы прочитать это, вам все еще нужно понять, как работают обещания.
- Обработка ошибок предшествует нормальному пути, что не является интуитивно понятным.
Заключение
Ключевые слова async/await
, введенные ES7, определенно являются улучшением асинхронного программирования JavaScript. Это может упростить чтение и отладку кода. Однако, чтобы использовать их правильно, нужно полностью понимать обещания, поскольку они не более чем синтаксический сахар, а лежащая в их основе техника по-прежнему остается обещаниями.
Надеюсь, этот пост даст вам некоторое представление о самих async/await
и поможет предотвратить некоторые типичные ошибки. Спасибо за чтение, и, пожалуйста, хлопайте мне в ладоши, если вам понравился этот пост.