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 |
瀏覽器相容性
載入中…