await using

可用性有限

此特性不是基线特性,因为它在一些最广泛使用的浏览器中不起作用。

await using 声明了块级作用域的局部变量,这些变量会被异步处置。与 const 类似,用 await using 声明的变量必须被初始化且不能被重新赋值。变量的值必须是 nullundefined,或是一个带有 [Symbol.asyncDispose]()[Symbol.dispose]() 方法的对象。当变量超出作用域时,对象的 [Symbol.asyncDispose]()[Symbol.dispose]() 方法会被调用并等待(await),以确保资源被释放。

语法

js
await using name1 = value1;
await using name1 = value1, name2 = value2;
await using name1 = value1, name2 = value2, /* …, */ nameN = valueN;
nameN

要声明的变量名。每个变量名都必须是合法的 JavaScript 标识符,并且不能解构绑定模式

valueN

变量的初始值。它可以是任何合法的表达式,但其值必须是 nullundefined,或是一个带有 [Symbol.asyncDispose]()[Symbol.dispose]() 方法的对象。

描述

此声明只能在 awaitusing 都可以使用的地方使用,包括:

  • 内部(如果该块也在异步上下文中)
  • async function 或 async generator function 的函数体内部
  • 模块 的顶层
  • forfor...of(如果 for 循环也在异步上下文中)或 for await...of 循环的初始化器中

await using 声明了一个异步可处置资源,它与变量作用域(块、函数、模块等)的生命周期绑定。当作用域退出时,资源会被异步处置。其语法可能有些令人困惑,因为 await 在变量首次声明时没有等待效果,只有当变量超出作用域时才会有等待效果。

当变量首次声明且其值非空时,会从对象中获取一个处置器(disposer)。首先尝试 [Symbol.asyncDispose] 属性,如果 [Symbol.asyncDispose]undefined,则回退到 [Symbol.dispose]。如果这两个属性都不包含函数,则会抛出 TypeError。值得注意的是,[Symbol.dispose]() 方法被封装成一个类似 async () => { object[Symbol.dispose](); } 的函数,这意味着如果它返回一个 Promise,该 Promise 不会被等待。这个处置器会被保存到作用域中。

当变量超出作用域时,处置器会被调用并等待。如果作用域包含多个 usingawait using 声明,所有处置器会以声明的相反顺序依次运行,无论声明的类型如何。所有处置器都保证会运行(很像 try...catch...finally 中的 finally 块)。所有在处置过程中抛出的错误,包括导致作用域退出的初始错误(如果适用),都会被聚合在一个 SuppressedError 中,其中较早的异常作为 suppressed 属性,较晚的异常作为 error 属性。这个 SuppressedError 在处置完成后抛出。

变量允许值为 nullundefined,因此资源可以可选地存在。只要在该作用域中声明了一个 await using 变量,即使该变量实际值为 nullundefined,也至少会保证在作用域退出时发生一次 await。这可以防止同步处置,从而避免时序问题(参见 await 的控制流效果)。

await using 将资源管理与词法作用域绑定,这既方便又有时令人困惑。请参阅下面的示例,了解它可能不会按预期运行的情况。如果你想手动管理资源处置,同时保持相同的错误处理保证,你可以使用 AsyncDisposableStack

示例

你还应该查看 using 以获取更多示例,尤其是关于基于作用域的资源管理的一些常见注意事项。

基本用法

通常,你会对一些库提供的、已经实现了异步可处置协议的资源使用 await using。例如,Node.js 的 FileHandle 就是异步可处置的。

js
import fs from "node:fs/promises";

async function example() {
  await using file = await fs.open("example.txt", "r");
  console.log(await file.read());
  // Before `file` goes out of scope, it is disposed by calling `file[Symbol.asyncDispose]()` and awaited.
}

请注意,file 的声明中有两个 await 操作,它们执行不同的任务,并且都是必需的。await fs.open() 会在获取期间导致一次等待:它会等待文件打开,并将返回的 Promise 解包为 FileHandle 对象。await using file 会在处置期间导致一次等待:它使得当变量超出作用域时,file 会被异步处置。

await usingfor await...of

以下三种语法很容易混淆:

  • for await (using x of y) { ... }
  • for (await using x of y) { ... }
  • for (using x of await y) { ... }

更令人困惑的是,它们可以一起使用。

js
for await (await using x of await y) {
  // ...
}

首先,await y 的作用和你预期的一样:我们 等待 Promise y,它应该解析为一个我们要迭代的对象。我们暂且把这个变体放在一边。

for await...of 循环要求 y 对象是一个异步可迭代对象。这意味着该对象必须有一个 [Symbol.asyncIterator] 方法,该方法返回一个异步迭代器,其 next() 方法返回一个表示结果的 Promise。这是用于当可迭代对象不知道下一个值是什么,甚至不知道是否已经完成,直到某个异步操作完成时的情况。

另一方面,await using x 语法要求从可迭代对象中产生的 x 对象是异步可处置的。这意味着该对象必须有一个 [Symbol.asyncDispose] 方法,该方法返回一个表示处置操作的 Promise。这与迭代本身是分开的关注点,并且仅当变量 x 超出作用域时才会被调用。

换句话说,以下所有四种组合都是有效的,并且执行不同的操作:

  • for (using x of y)y 同步迭代,每次产生一个结果,该结果可以同步处置。
  • for await (using x of y)y 异步迭代,等待后每次产生一个结果,但结果值可以同步处置。
  • for (await using x of y)y 同步迭代,每次产生一个结果,但结果值只能异步处置。
  • for await (await using x of y)y 异步迭代,等待后每次产生一个结果,并且结果值只能异步处置。

下面,我们创建了一些虚构的 y 值来演示它们的用例。对于异步 API,我们的代码基于 Node.js fs/promises 模块。

js
const syncIterableOfSyncDisposables = [
  stream1.getReader(),
  stream2.getReader(),
];
for (using reader of syncIterableOfSyncDisposables) {
  console.log(reader.read());
}
js
async function* requestMany(urls) {
  for (const url of urls) {
    const res = await fetch(url);
    yield res.body.getReader();
  }
}
const asyncIterableOfSyncDisposables = requestMany([
  "https://example.com",
  "https://example.org",
]);
for await (using reader of asyncIterableOfSyncDisposables) {
  console.log(reader.read());
}
js
const syncIterableOfAsyncDisposables = fs
  .globSync("*.txt")
  .map((path) => fs.open(path, "r"));
for (await using file of syncIterableOfAsyncDisposables) {
  console.log(await file.read());
}
js
async function* globHandles(pattern) {
  for await (const path of fs.glob(pattern)) {
    yield await fs.open(path, "r");
  }
}
const asyncIterableOfAsyncDisposables = globHandles("*.txt");
for await (await using file of asyncIterableOfAsyncDisposables) {
  console.log(await file.read());
}

作用域退出时的隐式等待

只要在作用域中声明了一个 await using,该作用域在退出时总是会有一个 await,即使变量是 nullundefined。这确保了稳定的执行顺序和错误处理。await 的控制流效果 示例对此有更多详细信息。

在下面的例子中,由于函数返回时存在隐式 await,因此 example() 调用直到下一个 tick 才解析。

js
async function example() {
  await using nothing = null;
  console.log("Example call");
}

example().then(() => console.log("Example done"));
Promise.resolve().then(() => console.log("Microtask done"));
// Output:
// Example call
// Microtask done
// Example done

考虑相同的代码,但使用同步的 using 代替。这次,example() 调用会立即解析,因此两个 then() 处理程序会在同一个 tick 中被调用。

js
async function example() {
  using nothing = null;
  console.log("Example call");
}

example().then(() => console.log("Example done"));
Promise.resolve().then(() => console.log("Microtask done"));
// Output:
// Example call
// Example done
// Microtask done

为了一个更真实的例子,考虑对一个函数的两个并发调用:

js
class Resource {
  #name;
  constructor(name) {
    this.#name = name;
  }
  async [Symbol.asyncDispose]() {
    console.log(`Disposing resource ${this.#name}`);
  }
}

async function example(id, createOptionalResource) {
  await using required = new Resource(`required ${id}`);
  await using optional = createOptionalResource
    ? new Resource("optional")
    : null;
  await using another = new Resource(`another ${id}`);
}

example(1, true);
example(2, false);
// Output:
// Disposing resource another 1
// Disposing resource another 2
// Disposing resource optional
// Disposing resource required 1
// Disposing resource required 2

如你所见,required 2 资源与 required 1 在同一个 tick 中被处置。如果 optional 资源没有导致冗余的 await,那么 required 2 会更早被处置,与 optional 同时。

规范

规范
ECMAScript 异步显式资源管理
# prod-AwaitUsingDeclaration

浏览器兼容性

另见