Высокоуровневый `await`
Высокоуровневый await
позволяет разработчикам использовать ключевое слово await
за пределами асинхронных функций. Он действует как большая асинхронная функция, заставляя другие модули, которые их import
, ожидать перед началом выполнения своего тела.
Старая модель поведения
Когда async
/await
впервые были представлены, попытка использовать await
за пределами async
функции приводила к SyntaxError
. Многие разработчики использовали немедленно вызываемые выражения асинхронных функций, чтобы получить доступ к этой функциональности.
await Promise.resolve(console.log('🎉'));
// → SyntaxError: await доступен только внутри async функций
(async function() {
await Promise.resolve(console.log('🎉'));
// → 🎉
}());
Новая модель поведения
С внедрением топ-уровневого await
, приведенный выше код теперь работает так, как ожидалось, внутри модулей:
await Promise.resolve(console.log('🎉'));
// → 🎉
Примечание: Высокоуровневый await
работает только на верхнем уровне модулей. Он не поддерживается в классических скриптах или неасинхронных функциях.
Примеры использования
Эти примеры использования взяты из репозитория предложений спецификации.
Динамическое определение зависимости
const strings = await import(`/i18n/${navigator.language}`);
Это позволяет модулям использовать значения времени выполнения для определения зависимостей. Это полезно, например, для разделения разработки/продакшна, интернационализации, разделения среды и так далее.
Инициализация ресурсов
const connection = await dbConnector();
Это позволяет модулям представлять ресурсы, а также уведомлять об ошибках в тех случаях, когда модуль нельзя использовать.
Резервные варианты зависимости
Следующий пример пытается загрузить JavaScript-библиотеку с CDN A, а в случае неудачи переключается на CDN B:
let jQuery;
try {
jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.example.com/jQuery');
}
Порядок выполнения модулей
Одним из самых значительных изменений в JavaScript с появлением топ-уровневого await
является порядок выполнения модулей в вашем графе. Движок JavaScript выполняет модули в порядке обратного обхода дерева: начиная с самой левой поддеревья вашего графа модулей, модули оцениваются, их привязки экспортируются, их соседи выполняются, а затем их родители. Этот алгоритм выполняется рекурсивно, пока не выполнится корневая часть вашего графа модулей.
До появления топ-уровневого await
этот порядок был всегда синхронным и детерминированным: при повторных запусках вашего кода граф гарантированно выполнялся в одном и том же порядке. Как только топ-уровневый await
внедрён, это же гарантируется, но только если вы не используете топ-уровневый await
.
Вот что происходит, когда вы используете топ-уровневый await
в модуле:
- Выполнение текущего модуля откладывается до тех пор, пока выполняемое обещание не разрешится.
- Выполнение родительского модуля откладывается до тех пор, пока завершающийся модуль, вызвавший
await
, и все его партнеры не экспортируют привязки. - Модули-родственники и родственники родительских модулей могут продолжить выполняться в том же синхронном порядке — если нет циклов или других ожиданий обещаний в графе.
- Модуль, вызвавший
await
, возобновляет выполнение после разрешения обещания. - Родительский модуль и последующие ветви продолжают выполняться в синхронном порядке, если нет других обещаний с
await
.
Разве это уже не работает в DevTools?
Да, работает! REPL в Chrome DevTools, Node.js и Safari Web Inspector уже давно поддерживает топ-уровневый await
. Однако эта функциональность была нестандартной и ограниченной REPL! Это отличается от предложения топ-уровневого await
, которое является частью спецификации языка и применяется только к модулям. Для тестирования продакшн-кода с использованием топ-уровневого await
убедитесь, что тестируете его в реальном приложении, а не только в DevTools или Node.js REPL!
Не является ли топ-уровневый await
потенциально опасным?
Возможно, вы видели печально известный gist от Рича Харриса, который изначально описывал ряд опасений по поводу использования await
на уровне верхнего уровня и призывал язык JavaScript не реализовывать эту функцию. Конкретные опасения были следующими:
await
на верхнем уровне может блокировать выполнение.await
на верхнем уровне может блокировать получение ресурсов.- Не будет ясной истории совместимости для модулей CommonJS.
Проект на стадии 3 напрямую решает эти вопросы:
- Так как соседние модули могут выполняться, полный блок отсутствует.
await
на верхнем уровне происходит на этапе выполнения графа модулей. На этом этапе все ресурсы уже были загружены и связаны. Нет риска блокировки загрузки ресурсов.await
на верхнем уровне ограничивается только модулями. Поддержка для скриптов или модулей CommonJS явно отсутствует.
Как и с любой новой функциональностью языка, всегда существует риск неожиданного поведения. Например, с await
на верхнем уровне циклические зависимости модулей могут привести к взаимной блокировке.
Без await
на верхнем уровне разработчики JavaScript часто использовали асинхронные немедленно вызываемые функциональные выражения, чтобы получить доступ к await
. К сожалению, этот паттерн приводит к меньшей предсказуемости выполнения графа и статической анализируемости приложений. По этим причинам отсутствие await
на верхнем уровне рассматривалось как больший риск, чем опасности, связанные с данной функцией.