- PVSM.RU - https://www.pvsm.ru -
Если вы не в курсе, в Node.js, начиная с версии 7.6, встроена поддержка механизма async/await. Говорят о нём, конечно, уже давно, но одно дело, когда для использования некоей функциональности нужны «костыли», и совсем другое, когда всё это идёт, что называется, «из коробки». Если вы ещё не пробовали async/await — обязательно попробуйте.
Сегодня мы рассмотрим шесть особенностей async/await, позволяющих отнести новый подход к написанию асинхронного кода к разряду инструментов, которые стоит освоить и использовать везде, где это возможно, заменив ими то, что было раньше.
Для тех, кто не знаком с async/await, вот основные вещи, которые полезно будет знать прежде чем двигаться дальше.
Представим, что у нас имеется функция getJSON
, которая возвращает промис, при успешном разрешении которого возвращается JSON-объект. Мы хотим эту функцию вызвать, вывести в лог JSON-объект и вернуть done
.
С использованием промисов подобное можно реализовать так:
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
makeRequest()
Вот как то же самое делается с использованием async/await:
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
makeRequest()
Если сопоставить два вышеприведённых фрагмента кода, можно обнаружить следующее:
async
. Ключевое слово await
можно использовать только в функциях, определённых с использованием async
. Любая подобная функция неявно возвращает промис, а значением, возвращённым при разрешении этого промиса, будет то, что возвратит инструкция return
, в нашем случае это строка done
.
await
нельзя использовать вне async-функций, на верхнем уровне кода.
// Эта конструкция на верхнем уровне кода работать не будет
// await makeRequest()
// А такая - будет
makeRequest().then((result) => {
// do something
})
await getJSON()
означает, что вызов console.log
будет ожидать разрешения промиса getJSON()
, после чего выведет то, что возвратит функция.
Рассмотрим обещанные шесть преимуществ async/await перед традиционными промисами.
Сравнивая два вышеприведённых примера, обратите внимание на то, насколько тот, где используется async/await, короче. И ведь речь, в данном случае, идёт о маленьких кусках кода, а если говорить о реальных программах, экономия будет ещё больше. Всё дело в том, что не нужно писать .then
, создавать анонимную функцию для обработки ответа, или включать в код переменную с именем data
, которая нам, по сути, не нужна. Кроме того, мы избавились от вложенных конструкций. Полезность этих мелких улучшений станет заметнее, когда мы рассмотрим другие примеры.
Конструкция async/await наконец сделала возможной обработку синхронных и асинхронных ошибок с использованием одного и того же механизма — старого доброго try/catch
. В следующем примере с промисами try/catch
не обработает сбой, возникший при вызове JSON.parse
, так как он выполняется внутри промиса. Для обработки подобной ошибки нужно вызвать метод .catch()
промиса и продублировать в нём код обработки ошибок. В коде рабочего проекта обработка ошибок будет явно сложнее вызова console.log
из примера.
const makeRequest = () => {
try {
getJSON()
.then(result => {
// парсинг JSON может вызвать ошибку
const data = JSON.parse(result)
console.log(data)
})
// раскомментируйте этот блок для обработки асинхронных ошибок
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
Вот то же самое, переписанное с использованием async/await. Блок catch
теперь будет обрабатывать и ошибки, возникшие при парсинге JSON.
const makeRequest = async () => {
try {
// парсинг JSON может вызвать ошибку
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}
Представьте, что надо написать кусок кода, который, загрузив некие данные, принимает решение о том, вернуть ли их в точку вызова, или, основываясь на том, что уже получено, запросить ещё что-нибудь. Решить подобную задачу можно так:
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
Только от взгляда на эту конструкцию может разболеться голова. Очень легко потеряться во вложенных конструкциях (тут их 6 уровней), скобках, командах возврата, которые нужны лишь для того, чтобы доставить итоговый результат главному промису.
Код будет гораздо легче читать, если решить задачу с использованием async/await.
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}
Возможно, вы встречались с ситуацией, когда вы вызываете promise1
, затем используете то, что он возвращает, для вызова promise2
, потом задействуете оба результата от ранее вызванных промисов для вызова promise3
. Вот как будет выглядеть код, решающий такую задачу.
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}
Если для promise3
не нужно value1
, можно без особых сложностей упростить структуру программы, особенно если вам режут глаз подобные конструкции, использованные без необходимости. В такой ситуации можно обернуть value1
и value2
в вызов Promise.all
и избежать ненужных вложенных конструкций.
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
// do something
return promise3(value1, value2)
})
}
При таком подходе семантика приносится в жертву читабельности кода. Нет причин для того, чтобы помещать value1
и value2
в один и тот же массив за исключением того, чтобы избежать вложенности промисов.
То же самое можно написать с применением async/await, причём делается это удивительно просто, а то, что получается, оказывается интуитивно понятным. Тут поневоле задумаешься о том, сколько полезного можно сделать за то время, которое тратится на написание хоть сколько-нибудь приличного кода с использованием промисов.
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
Представьте себе фрагмент кода, в котором имеется цепочка вызовов промисов, а где-то в этой цепочке выбрасывается ошибка.
const makeRequest = () => {
return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => {
throw new Error("oops");
})
}
makeRequest()
.catch(err => {
console.log(err);
// вывод
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
Стек ошибки, возвращённый из цепочки промисов, не содержит указания на то, где именно произошла ошибка. Более того, сообщение об ошибке способно направить усилия по поиску проблемы по по ложному пути. Единственное имя функции, которое содержится в сообщении — callAPromise
, а эта функция никаких ошибок не вызывает (хотя, конечно, тут есть и полезная информация — сведения о файле, где произошла ошибка, и о номере строки).
Если же взглянуть на подобную ситуацию при использовании async/await, стек ошибки укажет на ту функцию, в которой возникла проблема.
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
}
makeRequest()
.catch(err => {
console.log(err);
// вывод
// Error: oops at makeRequest (index.js:7:9)
})
Подобное нельзя назвать огромным плюсом, если речь идёт о разработке в локальном окружении, когда файл с кодом открыт в редакторе. Но это оказывается весьма полезным, если вы пытаетесь понять причину ошибки, анализируя лог-файл, полученный с продакшн-сервера. В подобных случаях знать, что ошибка произошла в makeRequest
, лучше, чем знать, что источник ошибки — некий then
, вызванный после ещё одного then
, который следует за ещё каким-то then
…
Этот пункт последний, но это не значит, что он не особо важен. Ценнейшая особенность использования async/await заключается в том, что код, в котором задействована эта конструкция, гораздо легче отлаживать. Отладка промисов всегда была кошмаром по двум причинам.
1. Нельзя установить точку останова в стрелочных функциях, которые возвращают результаты выполнения выражений (нет тела функции).
Попробуйте поставить где-нибудь в этом коде точку останова
2. Если вы установите точку останова внутри блока .then
и воспользуетесь командой отладчика вроде «шаг с обходом» (step-over), отладчик не перейдёт к следующему .then
, так как он может «перешагивать» только через синхронные блоки кода.
При использовании async/await особой нужды в стрелочных функциях нет, и можно «шагать» по вызовам, выполненным с ключевым словом await
так же, как это делается при обычных синхронных вызовах.
Отладка при использовании async/await
Вполне возможно, у вас возникнут некоторые соображения не в пользу применения async/await, продиктованные здоровым скептицизмом. Прокомментируем пару наиболее вероятных.
.then
, может потратить несколько недель на то, чтобы перестроиться на автоматическое восприятие инструкций async/await. Однако, например, в C# подобная функциональность есть уже многие годы, и те, кто с этим знакомы, знают, что польза от неё стоит временных неудобств при чтении кода.
Пожалуй, async/await — это одна из самых полезных революционных возможностей, добавленных в JavaScript в последние несколько лет. Она даёт простые и удобные способы написания и отладки асинхронного кода. Полагаем, async/await пригодится всем, кому приходится писать такой код.
Уважаемые читатели! Как вы относитесь к async/await? Пользуетесь ли вы этой возможностью в своих проектах?
Автор: RUVDS.com
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/252246
Ссылки в тексте:
[1] Источник: https://habrahabr.ru/post/326074/
Нажмите здесь для печати.