await using
await using
声明了块级作用域的局部变量,这些变量会被异步处置。与 const
类似,用 await using
声明的变量必须被初始化且不能被重新赋值。变量的值必须是 null
、undefined
,或是一个带有 [Symbol.asyncDispose]()
或 [Symbol.dispose]()
方法的对象。当变量超出作用域时,对象的 [Symbol.asyncDispose]()
或 [Symbol.dispose]()
方法会被调用并等待(await),以确保资源被释放。
语法
await using name1 = value1;
await using name1 = value1, name2 = value2;
await using name1 = value1, name2 = value2, /* …, */ nameN = valueN;
nameN
valueN
-
变量的初始值。它可以是任何合法的表达式,但其值必须是
null
、undefined
,或是一个带有[Symbol.asyncDispose]()
或[Symbol.dispose]()
方法的对象。
描述
此声明只能在 await
和 using
都可以使用的地方使用,包括:
- 在 块 内部(如果该块也在异步上下文中)
- 在 async function 或 async generator function 的函数体内部
- 在 模块 的顶层
- 在
for
、for...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 不会被等待。这个处置器会被保存到作用域中。
当变量超出作用域时,处置器会被调用并等待。如果作用域包含多个 using
或 await using
声明,所有处置器会以声明的相反顺序依次运行,无论声明的类型如何。所有处置器都保证会运行(很像 try...catch...finally
中的 finally
块)。所有在处置过程中抛出的错误,包括导致作用域退出的初始错误(如果适用),都会被聚合在一个 SuppressedError
中,其中较早的异常作为 suppressed
属性,较晚的异常作为 error
属性。这个 SuppressedError
在处置完成后抛出。
变量允许值为 null
或 undefined
,因此资源可以可选地存在。只要在该作用域中声明了一个 await using
变量,即使该变量实际值为 null
或 undefined
,也至少会保证在作用域退出时发生一次 await
。这可以防止同步处置,从而避免时序问题(参见 await
的控制流效果)。
await using
将资源管理与词法作用域绑定,这既方便又有时令人困惑。请参阅下面的示例,了解它可能不会按预期运行的情况。如果你想手动管理资源处置,同时保持相同的错误处理保证,你可以使用 AsyncDisposableStack
。
示例
你还应该查看 using
以获取更多示例,尤其是关于基于作用域的资源管理的一些常见注意事项。
基本用法
通常,你会对一些库提供的、已经实现了异步可处置协议的资源使用 await using
。例如,Node.js 的 FileHandle
就是异步可处置的。
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 using
与 for await...of
以下三种语法很容易混淆:
for await (using x of y) { ... }
for (await using x of y) { ... }
for (using x of await y) { ... }
更令人困惑的是,它们可以一起使用。
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
模块。
const syncIterableOfSyncDisposables = [
stream1.getReader(),
stream2.getReader(),
];
for (using reader of syncIterableOfSyncDisposables) {
console.log(reader.read());
}
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());
}
const syncIterableOfAsyncDisposables = fs
.globSync("*.txt")
.map((path) => fs.open(path, "r"));
for (await using file of syncIterableOfAsyncDisposables) {
console.log(await file.read());
}
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
,即使变量是 null
或 undefined
。这确保了稳定的执行顺序和错误处理。await 的控制流效果 示例对此有更多详细信息。
在下面的例子中,由于函数返回时存在隐式 await
,因此 example()
调用直到下一个 tick 才解析。
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 中被调用。
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
为了一个更真实的例子,考虑对一个函数的两个并发调用:
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 |
浏览器兼容性
加载中…