前些天 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 // 多资源对象声明
r1
、r2
、r3
是常量,同时也是具有块作用域的资源对象,在作用域结束时 同步
调用各自的 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 of
或 for 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
r1
、r2
、r3
是常量,同时也是具有块作用域的资源对象,在作用域结束时调用各自的 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 end
、2
、default
、3
。
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 of
或 for 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 标准增加了两个全局对象:DisposableStack
和 AsyncDisposableStack
。它们可以作为一个容器把多个资源对象聚合在一起。顾名思义,这个容器就是栈。当容器释放时,会按照先进后出的顺序释放容器中的每一个资源对象。
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()
}