在现代 JavaScript 中使用顶层 await

简介
如果您曾经为了在模块级别使用 await
而将异步代码包装在立即调用函数表达式(IIFE)中,那么您并不孤单。在 ES2022 之前,JavaScript 开发者必须费尽周折才能在模块初始化期间处理异步操作。JavaScript 顶层 await 通过允许在 ES 模块中直接使用 await
而无需 async
函数包装器来改变这一现状。
本文将解释顶层 await 如何改变模块执行、其在配置加载和动态导入中的实际应用,以及您需要了解的关键权衡——包括执行阻塞和循环依赖陷阱。您将学会何时使用这个强大的功能,以及同样重要的是,何时避免使用它。
关键要点
- 顶层 await 允许在 ES 模块中直接使用
await
,无需包装在异步函数中 - 模块执行变为异步,会阻塞依赖模块直到完成
- 最适用于一次性初始化、配置加载和条件导入
- 避免在库和工具中使用,以防止阻塞下游消费者
- 需要 ES 模块和现代运行时支持(Node.js 14.8+,ES2022)
什么是顶层 await,为什么要引入它?
它解决的问题
在顶层 await 之前,使用异步数据初始化模块需要变通方法:
// 使用 IIFE 的旧方法
let config;
(async () => {
config = await fetch('/api/config').then(r => r.json());
})();
// 访问时 config 可能是 undefined!
这种模式会造成时序问题,并使代码更难理解。模块无法保证在导出值之前其异步依赖已准备就绪。
ES2022 解决方案
顶层 await 允许在模块作用域中直接使用 await
表达式:
// 现代方法
const config = await fetch('/api/config').then(r => r.json());
export { config }; // 导入时总是已定义
此功能专门在 ES 模块中工作——具有 .mjs
扩展名的文件,或在 package.json 中设置了 "type": "module"
的项目中的 .js
文件。在浏览器中,脚本必须使用 <script type="module">
。
顶层 await 如何改变模块执行
模块加载变为异步
当 JavaScript 遇到异步函数外的 await 时,它从根本上改变了该模块的加载方式:
- 解析阶段:引擎验证语法并识别导入/导出
- 实例化阶段:创建模块绑定但不进行求值
- 求值阶段:执行代码,在每个
await
处暂停
// database.js
console.log('1. 开始连接');
export const db = await connectDB();
console.log('2. 连接就绪');
// app.js
console.log('3. 应用启动');
import { db } from './database.js';
console.log('4. 使用数据库');
// 输出顺序:
// 1. 开始连接
// 3. 应用启动
// 2. 连接就绪
// 4. 使用数据库
级联效应
模块依赖会产生连锁反应。当模块使用顶层 await 时,每个导入它的模块——无论是直接还是间接——都会等待完成:
// config.js
export const settings = await loadSettings();
// auth.js
import { settings } from './config.js';
export const apiKey = settings.apiKey;
// main.js
import { apiKey } from './auth.js'; // 等待整个链条
常见用例和模式
动态模块加载
JavaScript 顶层 await 在基于运行时条件的条件导入方面表现出色:
// 根据环境加载数据库驱动
const dbModule = await import(
process.env.DB_TYPE === 'postgres'
? './drivers/postgres.js'
: './drivers/mysql.js'
);
export const db = new dbModule.Database();
配置和资源初始化
非常适合在模块执行前加载配置或初始化资源:
// i18n.js
const locale = await detectUserLocale();
const translations = await import(`./locales/${locale}.js`);
export function t(key) {
return translations.default[key] || key;
}
WebAssembly 模块加载
简化 WASM 初始化,无需包装函数:
// crypto.js
const wasmModule = await WebAssembly.instantiateStreaming(
fetch('/crypto.wasm')
);
export const { encrypt, decrypt } = wasmModule.instance.exports;
Discover how at OpenReplay.com.
关键限制和权衡
仅限 ES 模块
顶层 await 有严格的上下文要求:
// ❌ CommonJS - 语法错误
const data = await fetchData();
// ❌ 经典脚本 - 语法错误
<script>
const data = await fetchData();
</script>
// ✅ ES 模块
<script type="module">
const data = await fetchData();
</script>
执行阻塞
每个 await
都会创建一个同步点,可能影响应用程序启动:
// slow-module.js
export const data = await fetch('/slow-endpoint'); // 5 秒延迟
// app.js
import { data } from './slow-module.js';
// 整个应用在此行运行前等待 5 秒
循环依赖死锁
顶层 await 使循环依赖更加危险:
// user.js
import { getPermissions } from './permissions.js';
export const user = await fetchUser();
// permissions.js
import { user } from './user.js';
export const permissions = await getPermissions(user.id);
// 结果:死锁 - 模块无限期地相互等待
生产使用的最佳实践
何时使用顶层 await
- 一次性初始化:数据库连接、API 客户端
- 配置加载:特定环境的设置
- 功能检测:有条件地加载 polyfills
何时避免使用
- 库模块:永远不要阻塞下游消费者
- 频繁导入的工具:保持同步以提高性能
- 有循环依赖风险的模块:改用异步函数
错误处理策略
始终处理失败以防止模块加载崩溃:
// 带回退的安全模式
export const config = await loadConfig().catch(err => {
console.error('配置加载失败:', err);
return { defaultSettings: true };
});
// 替代方案:让消费者处理错误
export async function getConfig() {
return await loadConfig();
}
构建工具和运行时支持
现代工具以不同方式处理 JavaScript 顶层 await:
- Webpack 5+:通过
experiments.topLevelAwait
支持 - Vite:在开发和生产中原生支持
- Node.js 14.8+:在 ES 模块中完全支持
- TypeScript 3.8+:需要
module: "es2022"
或更高版本
对于传统环境,考虑将异步逻辑包装在导出函数中,而不是使用顶层 await。
结论
顶层 await 改变了我们在 JavaScript 中编写异步模块初始化的方式,消除了 IIFE 变通方法,使代码更具可读性。然而,它的强大功能伴随着责任——阻塞模块执行和潜在的循环依赖问题需要仔细考虑。
将顶层 await 用于特定应用程序的初始化和配置加载,但要将其排除在共享库和工具之外。通过理解其能力和约束,您可以利用此功能编写更清洁、更易维护的 JavaScript 模块,同时避免暂停模块执行带来的陷阱。
常见问题
不可以,顶层 await 只在 ES 模块中工作。在 Node.js 中,使用 .mjs 文件或在 package.json 中设置 type module。CommonJS 模块必须继续使用异步函数或 IIFE 进行异步操作。
顶层 await 本身不会阻止 tree shaking,但可能会影响包分割。打包工具可能会以不同方式对使用顶层 await 的模块进行分组以维持执行顺序,可能会创建更大的块。
大多数现代测试运行器都支持带有顶层 await 的 ES 模块。对于 Jest,启用实验性 ESM 支持。考虑模拟异步依赖或将初始化包装在函数中以便于测试。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.