using 关键字终于可以在 JavaScript 中使用啦!

前些天 Node 发布了 24 的新版本,其中把 V8 引擎升级到 13.6 的版本。在该版本中 V8 引擎包含了几个 JavaScript 新特性,其中就有 using 关键字。

我们今天就来探讨下他是用来做什么的。

资源管理

在开发中,我们会遇到创建各种资源(内存、I/O等)的场景,之后都会在使用完资源后进行清理工作。比如关闭文件句柄、网络连接等。

这里我们以 Node 中读取目录为例,在读取完之后需要关闭它。

js 复制代码
import * as fs from 'node:fs'

function main() {
  const path = 'xxx'

  // 1. 打开
  const dir = fs.opendirSync(path)

  // 2. 读取
  // ...

  // 3. 关闭
  dir.closeSync()
}

问题一 额外的 try finally

在关闭之前可能还有其他逻辑会导致提前结束。为了确保资源能够被正常的关闭,会在其他逻辑结束之前对资源进行关闭。

js 复制代码
import * as fs from 'node:fs'

function main() {
  const path = 'xxx'

  // 1. 打开
  const dir = fs.opendirSync(path)

  // 2. 读取
  // ...
  
  // 3. 其他条件
  if () {
    // ...
    // 关闭
    dir.closeSync()
    // 提前结束
    return
  }
  
  // ...

  // 4. 关闭
  dir.closeSync()
}

这样写会多出重复的代码,所以这里最好的做法就是使用 try finally 块进行包裹,把关闭资源的步骤放入 finally 块中。但是这样会引入额外的 try finally 块。

js 复制代码
import * as fs from 'node:fs'

function main() {
  const path = 'xxx'
  try {
    // 1. 打开
    const dir = fs.opendirSync(path)

    // 2. 读取
    // ...
    
    // 3. 其他条件
    if () {
      // ...
      // 提前结束
      return
    }
    
    // ...
    
  } finally {
    // 4. 关闭
    dir.closeSync()
  }
}

问题二 通用的清理接口

如果我们想要读取多个目录,那么在 finally 块中就需要关闭多个资源。

js 复制代码
import * as fs from 'node:fs'

function main() {
  const path1 = 'xxx'
  const path2 = 'xxx'
  
  try {
    // 1. 打开
    const dir1 = fs.opendirSync(path1)
    const dir2 = fs.opendirSync(path2)

    // ...

  } finally {
    // 4. 关闭
    dir1.closeSync()
    dir2.closeSync()
  }
}

如果关闭前一个资源时发生了异常就会阻碍后面的资源无法正常关闭,这个问题该怎么解决呢?

我们可以收集每个资源的关闭函数 ,在 finally 块中使用 while 循环统一执行关闭函数,然后使用 try catch块包裹,确保每个资源关闭函数都能够被执行

js 复制代码
import * as fs from 'node:fs'

function main() {
  const path1 = 'xxx'
  const path2 = 'xxx'
  
  const fns = []
  
  try {
    // 1. 打开
    const dir1 = fs.opendirSync(path1)
    fns.push([dir1, function () {
      this.closeSync()
    }])
    
    const dir2 = fs.opendirSync(path2)
    fns.push([dir2, function () {
      this.closeSync()
    }])
    
    // ...

  } finally {
    // 4. 关闭
    while (fns.length) {
      try {
        const [value, fn] = fns.pop()
        fn.call(value)
      } catch (err) {
      
      }
    }
  }
}

如果资源对象有一个统一的接口函数,那么就不需要我们收集资源的关闭函数,直接执行就好了。

问题三 更好的显示错误

细心的你会发现上面捕获到的错误没有处理,还有在读取目录过程中产生的错误也没有处理,这些错误肯定是要收集的,然后在关闭所有资源后重新抛出。

那么该如何保存这些错误呢?

SuppressedError

为了更好的显示这些错误,引入了一个新的 错误对象 SuppressedError,它是 Error 的子类。

SuppressedError 是一个构造函数,用来生成 SuppressedError 实例对象。

js 复制代码
SuppressedError(error, suppressed[, message])

SuppressedError() 构造函数可以接受三个参数。

  • error:错误实例对象,表示最近产生的错误,该参数是必须的。
  • suppressed:错误实例对象,表示上一个产生的错误,该参数是必须的。
  • message:字符串,抛出 SuppressedError 时的提示信息,该参数是可选的。

SuppressedError 的实例对象有四个属性。

  • name:错误名称,默认为"SuppressedError"。
  • error:最近产生的错误。
  • suppressed:上一个产生的错误。
  • message:错误的提示信息。

上面的 问题三 可以使用这个 SuppressedError 错误对象,把最近产生的错误赋值给 error 属性,上一个产生的错误赋值给 suppressed 属性,之后这个 SuppressedError 错误将成为上一个产生的错误,又把他赋值给了新的 SuppressedError 对象的 suppressed 属性,而新的 SuppressedError 对象的 error 属性保存最近一次产生的错误,就这样如果有多个错误,它们将被包装在嵌套的 SuppressedError 对象中,最后会把最新的 SuppressedError 错误实例抛出。

当然使用 SuppressedError 错误的前提是至少产生了两个错误,如果只产生一个错误的话,不需要使用 SuppressedError,直接把这个错误抛出就好。

js 复制代码
import * as fs from 'node:fs'

function main() {
  // ...
  let error = null
  try {
    // ...
  } catch (err1) {
    error = err1
  } finally {
    while (fns.length) {
      try {
        // ...
      } catch (err2) {
        error = error ? new SuppressedError(err2, error) : err2
      }
    }
    if (error) throw error
  }

}

当捕获到了 SuppressedError 对象,如果需要查看上一次的上一次产生的错误就需要通过 .suppressed.suppressed 访问查看了。

Disposable 接口

接下来引入一个新的、类型为 Symbol 的Symbol.dispose,它是 Symbol 对象的 dispose 属性。

只要对象具有 Symbol.dispose 属性,就可以认为它是"一次性的"(Disposable),或者说这个对象是一个资源对象。这个属性对应的值是一个函数,其作用就是对该对象执行清理工作

Disposable 接口的目的,就是为资源对象,提供了一种统一的执行清理的函数接口。

ts 复制代码
interface Disposable {
  [Symbol.dispose](): void;
}

那么 问题二 就可以直接使用该接口,因为 opendirSync 函数返回的 Dir 对象部署了该接口,也就是具有 Symbol.dispose 属性。

js 复制代码
import * as fs from 'node:fs'

function main() {
  const path1 = 'xxx'
  const path2 = 'xxx'
  
  const resources = []
  
  try {
    // 1. 打开
    const dir1 = fs.opendirSync(path1)
    resources.push(dir1)
    
    const dir2 = fs.opendirSync(path2)
    resources.push(dir2)
    
    // ...

  } finally {
    // 4. 关闭
    while (resources.length) {
      try {
        const resource = resources.pop()
        resource[Symbol.dispose]()
      } catch (err) {
      
      }
    }
  }
}

using

为了更加方便的管理资源对象,引入了一个新的关键字:using,其实是一个语法糖,它允许我们以声明式的方式管理资源。

声明式的意思就是和 const 关键字一样可以声明一个 常量,具有 块作用域。但区别在于使用 using 声明的常量会在作用域结束时 同步 调用其 Symbol.dispose 方法以释放资源

在块作用域结束时,会自动调用 dir1 的 Symbol.dispose 方法。

js 复制代码
{
  using dir1 = fs.opendirSync(path1)
} // 在块作用域结束时,dir1 将自动被关闭。

说到声明式还有个命令式,就是直接调用方法进行关闭的这种形式称为命令式。

js 复制代码
// ...
dir1[Symbol.dispose]()

用法

const 声明常量一样,当然也可以多资源声明。

js 复制代码
using r1 = exp1
using r2 = exp2, r3 = exp3 // 多资源对象声明

r1r2r3 是常量,同时也是具有块作用域的资源对象,在作用域结束时 同步 调用各自的 Symbol.dispose 方法。

在 switch 语句中的 case 块中使用 using 时,此时 using 所在的作用域就是 case 块作用域,所以当退出这个作用域时会释放资源 a。

js 复制代码
function createResource(id) {
  return {
    id,
    [Symbol.dispose]() {
      console.log(`dispose ${this.id}`)
    }
  }
}

const value = 1
switch (value) {
  case 1: {
    using x = createResource('a')
    console.log('1')
  }
  case 2: {
    console.log('2')
  }
  default: {
    console.log('default')
  }
  case 3: {
    console.log('3')
  }
}

打印结果

text 复制代码
1
dispose a
2
default
3

当把 case 1 删除块 {} 时,此时 using 所在的作用域就是 switch 块作用域,所以当退出 switch 块作用域后才会释放资源 a。

js 复制代码
const value = 1
switch (value) {
  case 1:
    using x = createResource('a')
    console.log('1')
  case 2: {
    console.log('2')
  }
  default: {
    console.log('default')
  }
  case 3: {
    console.log('3')
  }
}

打印结果

text 复制代码
1
2
default
3
dispose a

还可以在 for offor await of 循环的头部中使用 using 声明。

js 复制代码
for (using x of createResources()) {
  // ...
}

每一次循环结束时都会同步释放当前循环的资源对象。如果循环中有 break、throw、return 导致循环提前结束,此时并不会释放后面没有循环到的资源对象。

不能在 for in 循环的头部中使用 using 声明。

使用 using 声明的常量在赋值时会检查值对象是否具有 Disposable 接口,也就是是否有 Symbol.dispose 方法,如果没有会引发 TypeError 错误。

如果值是 null 或 undefined,这个 常量 会被 忽略,也就是不会进行检查,同时也不会收集这个常量。

js 复制代码
using r1 = null // 不会引发 TypeError 错误,会被忽略

// 错误,因为 r1 是常量 const
// r1 = exp1

本质

我们说下 using 关键字的本质,它其实就是一个语法糖。

js 复制代码
{
  // ... (1)
  using dir1 = fs.opendirSync(path1)
  using dir2 = fs.opendirSync(path2)
  // ... (2)
}

上面会被转化为下面这样,和我们在第一章节 资源管理 的逻辑是一样的,首先检查资源对象是否为 null 或 undefined 值,是的话直接忽略,不是就收集资源对象的 Symbol.dispose 方法,当然会检查这个 Symbol.dispose 属性值是否为函数,然后在最后 finally 块中使用 while 循环对收集的资源对象一一进行清理工作。

这个过程是一个栈的先进后出,按照资源对象的顺序收集,然后按照相反的顺序释放这些资源对象,后收集的会先执行清理工作

js 复制代码
{ // 块作用域
  const ctx = {
    stack: [],
    error: null
  }

  try {
    // ... (1)
    const dir1 = fs.opendirSync(path1)
    if (dir1 !== null && dir1 !== undefined) {
      const dispose = dir1[Symbol.dispose]
      if (typeof dispose !== 'function') {
        throw new TypeError()
      }
      ctx.stack.push({ value: dir1, dispose: dispose })
    }
  
    const dir2 = fs.opendirSync(path2)
    if (dir2 !== null && dir2 !== undefined) {
      const dispose = dir2[Symbol.dispose]
      if (typeof dispose !== 'function') {
        throw new TypeError()
      }
      ctx.stack.push({ value: dir2, dispose: dispose })
    }
    // ... (2)
  } catch (err1) {
    ctx.error = err1
  } finally {
    while (ctx.stack.length) {
      try {
        const { value, dispose } = ctx.stack.pop()
        dispose.call(value)
      } catch (err2) {
        ctx.error = ctx.error ? new SuppressedError(err2, ctx.error) : err2
      }
    }
  
    if (ctx.error) throw ctx.error
  }
}

这样一对比,是不是发现少写了好多的代码。

思考题

js 复制代码
function createResource(id) {
  console.log(`resource ${id} is created.`)
  
  return {
    [Symbol.dispose]() {
      console.log(`resource ${id} is disposed.`)
    }
  }
}

using a = createResource('a')

{
  using b = createResource('b')
}

using c = createResource('c')
return

using d = createResource('d')

思考下打印结果是什么?

text 复制代码
resource a is created.
resource b is created.
resource b is disposed.
resource c is created.
resource c is disposed.
resource a is disposed.

需要注意的是使用 using 声明,并不是在声明时创建资源,而是在运行时只有在运行时创建和收集,之后才能执行清理工作 。这一点可以看上面 using 的本质 章节中的代码发现。

AsyncDisposable 接口

你可能会注意到上面的例子中的关闭函数都是使用的同步方法,但是大多数情况下的资源清理函数都是 异步 的,需要等待其执行完成,然后才能继续执行其他代码。

下面引入一个新的、类型为 Symbol 的值:Symbol.asyncDispose,它是 Symbol 对象的 asyncDispose 属性。

Symbol.asyncDispose 属性函数返回一个 Promise 值。

ts 复制代码
interface AsyncDisposable {
  [Symbol.asyncDispose](): Promise<void>;
}

和 Symbol.dispose 是一样的,唯一的不同是它是为 异步 服务的,为了配合下面新的声明:await using

await using

与 using 声明一样,唯一不同的就是在执行清理工作时他会等待异步清理函数执行完成

对于 using,是收集资源对象的 Symbol.dispose 方法,而 await using 则先是收集 Symbol.asyncDispose 方法,如果没有,则会收集 Symbol.dispose 方法,还没有的话就会引发 TypeError 错误。

使用 await using 声明的常量会在作用域结束时调用其异步的 Symbol.asyncDispose 方法释放资源并且等待该异步函数执行完成

用法

也可以多资源对象声明。

js 复制代码
await using x = expr1
await using y = expr2, z = expr3

r1r2r3 是常量,同时也是具有块作用域的资源对象,在作用域结束时调用各自的 Symbol.asyncDispose 方法释放资源并且等待该异步函数执行完成

可以在 module 中任何变量声明语句的顶层位置使用 await using 声明。

在 switch 语句中的 case 块中使用 using 时,此时 await using 所在的作用域就是 case 块作用域,所以当退出这个作用域时会释放资源 a 并且等待清理函数执行完成。

html 复制代码
<script type="module">
function createAsyncResource(id) {
  return {
    id,
    async [Symbol.asyncDispose]() {
      onsole.log(`async dispose ${this.id} start`)
      await delay(3000)
      console.log(`async dispose ${this.id} end`)
    }
  }
}

const value = 1
switch (value) {
  case 1: {
    await using x = createAsyncResource('a')
    console.log('1')
  }
  case 2: {
    console.log('2')
  }
  default: {
    console.log('default')
  }
  case 3: {
    console.log('3')
  }
}
</script>

所以在退出 case 块作用域后需要等待 3 秒之后才输出 async dispose a end2default3

text 复制代码
1
async dispose a start
async dispose a end
2
default
3

当把 case 1 删除块 {} 时,此时 await using 所在的作用域就是 switch 块作用域,所以当退出 switch 块作用域后才会释放资源 a 并且等待清理函数执行完成。

html 复制代码
<script type="module">

const value = 1
switch (value) {
  case 1: 
    await using x = createAsyncResource('a')
    console.log('1')
  case 2: {
    console.log('2')
  }
  default: {
    console.log('default')
  }
  case 3: {
    console.log('3')
  }
}
</script>

3 秒之后打印 async dispose a end

text 复制代码
1
2
default
3
async dispose a start
async dispose a end

可以在异步函数或异步生成器内的任何变量声明语句的位置使用 await using 声明。

在退出函数作用域后会释放资源 a 并且等待清理函数执行完成。

js 复制代码
async function () {
  await using x = createAsyncResource('a')
}

for offor await of 循环的头部中使用。

html 复制代码
<script type="module">
  for (await using x of iterateResources()) {
    // 使用 x
  } // 当前循环结束异步释放 x 并且等待异步清理函数执行完成

  for await (await using x of asyncIterateResources()) {
    // 使用 x
  }
</script>

每一次循环结束时都会异步释放当前循环的资源对象并且等待异步清理函数执行完成才会进行下一次的循环。如果循环中有 break、throw、return 导致循环提前结束,此时并不会释放后面没有循环到的资源对象。

html 复制代码
<script type="module">
const asyncResources = [
  createAsyncResource('a'),
  createAsyncResource('b')
]

for (await using resource of asyncResources) {
  console.log(`使用资源 ${resource.id}`)
}
</script>

进入下一次循环都需要等待 3 秒。

text 复制代码
使用资源 a
async dispose a start
async dispose a end
使用资源 b
async dispose b start
async dispose b end

await using 声明不能在 for in 循环的头部中使用。

本质

js 复制代码
{
  // ... (1)
  await using x = expr1
  // ... (2)
}

和 using 不同的是,首先会先收集 Symbol.asyncDispose 方法,如果没有,则会收集 Symbol.dispose 方法,还没有的话就会引发 TypeError 错误。其次就是在释放资源时会等待异步清理函数执行完成。

如果使用 await using 声明,但是资源对象只有 Symbol.dispose 方法时,那么这里会把 Symbol.dispose 方法使用 async 函数包裹一下的

js 复制代码
{
  const ctx = { stack: [], error: null }
  try {
    // ... (1)

    const x = expr1
    if (x !== null && x !== undefined) {
      let asyncDispose = x[Symbol.asyncDispose]
      if (typeof asyncDispose !== 'function') {
        const dispose = x[Symbol.dispose]
        if (typeof dispose !== 'function') {
          throw new TypeError()
        }
        
        // 使用 async 函数包裹
        asyncDispose = async function () {
          dispose.call(this)
        }
      }
      
      ctx.stack.push({ value: x, dispose: asyncDispose })
    }

    // ... (2)
  } catch (err1) {
    ctx.error = err1
  } finally {
    while (ctx.stack.length) {
      const { value, dispose } = ctx.stack.pop()
      try {
        await dispose.call(value) // 等待异步函数执行完成
      }
      catch (err2) {
        ctx.error = ctx.error ? new SuppressedError(err2, ctx.error) : err2
      }
    }
    if (ctx.error) throw ctx.error
  }
}

DisposableStack 和 AsyncDisposableStack 容器对象

ES 标准增加了两个全局对象:DisposableStackAsyncDisposableStack 。它们可以作为一个容器把多个资源对象聚合在一起。顾名思义,这个容器就是栈。当容器释放时,会按照先进后出的顺序释放容器中的每一个资源对象。

DisposableStack

DisposableStack 原生添加了 Disposable 接口,所以我们可以使用 using 声明实例对象。

如果容器中的资源对象在清理期间引发错误,则会在释放完所有资源对象后重新引发该错误(如果清理期间多个资源对象的清理函数都引发了错误,则它们会被包装在嵌套的 SuppressedError 中)。

DisposableStack.prototype.use

use 方法用于将具有 Disposable 接口的资源对象添加到栈容器的顶部。如果该参数值是 null 或 undefined,则会被忽略。其实就是收集资源对象的 Symbol.dispose 方法。use 方法的返回值就是参数值。

当容器 stack 释放时,栈中的资源会按照栈的先进后出顺序释放:resource3 、resource2 、resource1。

js 复制代码
const stack = new DisposableStack()
const resource1 = stack.use(getResource1())
const resource2 = stack.use(getResource2())
const resource3 = stack.use(getResource3())
stack[Symbol.dispose]()

使用 using 更简单些。

js 复制代码
using stack = new DisposableStack()
const resource1 = stack.use(getResource1())
const resource2 = stack.use(getResource2())
const resource3 = stack.use(getResource3())

如果在释放资源期间,resource1 、resource2 、resource3 都引发了错误,则会产生下面嵌套的 SuppressedError 错误。

js 复制代码
new SuppressedError(
  /*error*/ exception_from_resource1_disposal,
  /*suppressed*/ new SuppressedError(
    /*error*/ exception_from_resource2_disposal,
    /*suppressed*/ exception_from_resource3_disposal
  )
)

DisposableStack.prototype.adopt

当资源对象没有 Disposable 接口时,但又想通过容器集中统一管理,那么此时可以使用 adopt 方法。它也是将资源对象和回调函数添加到栈容器的顶部。

该方法接受两个参数,第一个参数为资源对象,第二个参数为释放时执行的回调函数。回调函数的参数和 adopt 方法的返回值都是第一个参数资源对象。

js 复制代码
{
  using stack = new DisposableStack()
  const reader = stack.adopt(createReader(), reader => reader.releaseLock())
  // ...
}

DisposableStack.prototype.defer

defer 方法可用于执行其他清理工作。该方法只接受一个回调函数。也是将回调函数添加到栈容器的顶部位置。

与 Go 语言中的 defer 关键字类似。

js 复制代码
function f() {
  using stack = new DisposableStack()
  console.log('enter')
  stack.defer(() => console.log('exit'))
  
  // ...
}

DisposableStack.prototype.move

move 方法用于管理容器中资源对象的生命周期。顾名思义,其实就是把容器中的资源对象移动到新的容器中,然后返回这个新容器。

有时候我们需要定义一个具有 Disposable 接口的类,然后在类构造器中创建各种需要的资源对象,我们需要对这些资源对象进行统一管理,所以使用了 DisposableStack,同时又因为在类构造器期间可能会产生错误,所以需要保证在发生错误之前释放容器中的资源对象,所以使用 using 声明,但这样会导致在类构造器作用域结束后会正常释放容器中的资源对象,我们想把这些资源对象给移出去,通过定义的这个类 Demo 来管理这些资源的生命周期,也就是说这些资源应在释放定义类 Demo 时释放,那么此时就可以使用 move 方法。

js 复制代码
class Demo {
  #disposed = false
  #resource1 = null
  #resource2 = null
  #disposables = null
  
  constructor() {
    using stack = new DisposableStack()
    this.#resource1 = stack.use(getResource1())
    this.#resource2 = stack.use(getResource2())
    
    this.#disposables = stack.move()
  }
  
  [Symbol.dispose]() {
    if (!this.#disposed) {
      this.#disposed = true
      const disposables = this.#disposables
      this.#resource1 = null
      this.#resource2 = null
      disposables.dispose()
    }
  }
}

DisposableStack.prototype.dispose

dispose 方法是 Symbol.dispose 方法的别名。用于按照栈的先进后出的顺序释放容器中所有的资源对象。

js 复制代码
const stack = new DisposableStack()
const resource1 = stack.use(getResource1())
const resource2 = stack.use(getResource2())
const resource3 = stack.use(getResource3())
stack.dispose()

AsyncDisposableStack

AsyncDisposableStack 原生添加了 AsyncDisposable 接口,所以我们可以使用 await using 声明实例对象。AsyncDisposableStack 是 DisposableStack 的异步版本,主要区别就是它是用来聚合具有 AsyncDisposable 接口的资源对象

如果容器中的资源对象在清理期间引发错误或者是清理函数返回的 promise 值变成 rejected 状态,则会在释放完所有资源对象后重新引发该错误(如果清理期间多个资源对象的清理函数都引发了错误,则它们会被包装在嵌套的 SuppressedError 中)。

AsyncDisposableStack.prototype.use

和 DisposableStack 的 use 方法一样,只不过在收集清理函数时会先收集 Symbol.asyncDispose 方法,如果没有,则会收集 Symbol.dispose 方法,还没有的话就会引发 TypeError 错误。

js 复制代码
const stack = new AsyncDisposableStack()
const resource1 = stack.use(getResource1())
const resource2 = stack.use(getResource2())
const resource3 = stack.use(getResource3())
await stack[Symbol.asyncDispose]()

使用 await using 更加简单

js 复制代码
await using stack = new AsyncDisposableStack()
const resource1 = stack.use(getResource1())
const resource2 = stack.use(getResource2())
const resource3 = stack.use(getResource3())

AsyncDisposableStack.prototype.adopt

当资源对象没有 AsyncDisposable 接口时,但又想通过容器集中统一管理,那么此时可以使用 adopt 方法。它也是将资源对象和回调函数添加到栈容器的顶部。

该方法接受两个参数,第一个参数为资源对象,第二个参数为释放时执行的回调函数。回调函数的参数和 adopt 方法的返回值都是第一个参数资源对象。

js 复制代码
{
  await using stack = new AsyncDisposableStack()
  const reader = stack.adopt(createAsyncReader(), async reader => await reader.releaseLock())
  // ...
}

AsyncDisposableStack.prototype.defer

defer 方法可用于执行其他清理工作。该方法只接受一个回调函数。也是将回调函数添加到栈容器的顶部位置。

与 Go 语言中的 defer 关键字类似。

js 复制代码
function f() {
  await using stack = new AsyncDisposableStack()
  console.log('enter')
  stack.defer(async () => console.log('exit'))
  
  // ...
}

AsyncDisposableStack.prototype.move

和 DisposableStack 的 move 方法一样。

因为不存在异步的构造函数,所以这里使用了静态的异步 create 方法,通过 Demo.create 调用。

js 复制代码
class Demo {
  #disposed = false
  #resource1 = null
  #resource2 = null
  #disposables = null
  
  constructor() {

  }
  
  async static create() {
    const inst = new Demo()
    await using stack = new AsyncDisposableStack()
    inst.#resource1 = stack.use(getResource1())
    inst.#resource2 = stack.use(getResource2())
    
    inst.#disposables = stack.move()
    
    return inst
  }
  
  async [Symbol.asyncDispose]() {
    if (!this.#disposed) {
      this.#disposed = true
      const disposables = this.#disposables
      this.#resource1 = null
      this.#resource2 = null
      await disposables.disposeAsync()
    }
  }
}

AsyncDisposableStack.prototype.disposeAsync

disposeAsync 方法是 Symbol.asyncDispose 方法的别名。

js 复制代码
const stack = new AsyncDisposableStack()
const resource1 = stack.use(getResource1())
const resource2 = stack.use(getResource2())
const resource3 = stack.use(getResource3())
await stack.disposeAsync()

disposeAsync 方法的简单实现

js 复制代码
AsyncDisposableStack.prototype.disposeAsync = AsyncDisposableStack.prototype[Symbol.asyncDispose] = async function () {
  const stack = this.#stack
  let error = null
  while (stack.length) {
    const { value, dispose } = stack
    try {
      await dispose.call(value)
    } catch (err) {
      error = error ? new SuppressedError(err, error) : err
    }
  }
  if (error) throw error
}

Iterator

ES 标准对遍历器对象(Iterator)原生添加Disposable 接口。

实际上就是调用自身的 return 方法。

js 复制代码
Iterator.prototype[Symbol.dispose] = function () {
  this.return()
}

AsyncIterator

对异步遍历器对象(AsyncIterator)原生添加AsyncDisposable 接口。

也是调用自身的 return 方法,这里会等待 return 方法执行完成。

js 复制代码
AsyncIterator.prototype[Symbol.asyncDispose] = async function () {
  await this.return()
}
相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax