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

瀏覽器相容性

另見