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;
nameNvalueN-
变量的初始值。它可以是任何合法的表达式,但其值必须是
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 |
浏览器兼容性
加载中…