Перейти к основному содержимому

Высокоуровневый `await`

· 4 мин. чтения
Майлз Боринс ([@MylesBorins](https://twitter.com/MylesBorins))

Высокоуровневый 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 в модуле:

  1. Выполнение текущего модуля откладывается до тех пор, пока выполняемое обещание не разрешится.
  2. Выполнение родительского модуля откладывается до тех пор, пока завершающийся модуль, вызвавший await, и все его партнеры не экспортируют привязки.
  3. Модули-родственники и родственники родительских модулей могут продолжить выполняться в том же синхронном порядке — если нет циклов или других ожиданий обещаний в графе.
  4. Модуль, вызвавший await, возобновляет выполнение после разрешения обещания.
  5. Родительский модуль и последующие ветви продолжают выполняться в синхронном порядке, если нет других обещаний с 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 на верхнем уровне рассматривалось как больший риск, чем опасности, связанные с данной функцией.

Поддержка await на верхнем уровне