Explicit Resource Management S4
中文标题:显式资源管理
提案速览
该提案为 ECMAScript 引入了用于显式、块作用域资源管理的 using 和 await using 声明,以及 Symbol.dispose、Symbol.asyncDispose 和 DisposableStack/AsyncDisposableStack 容器。它旨在标准化文件句柄和流等资源的清理模式。
Note
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
ECMAScript 显式资源管理
注意: 本提案已合并了 异步显式资源管理 提案。本提案仓库应用于进一步讨论显式资源管理的同步和异步方面。
该提案旨在解决软件开发中关于各种资源(内存、I/O 等)的生命周期和管理的常见模式。该模式通常包括资源的分配以及显式释放关键资源的能力。
例如,ECMAScript 生成器函数和异步生成器函数通过 return 方法暴露了这种模式,作为显式执行 finally 块以确保用户定义的清理逻辑得以保留的一种手段:
// 同步生成器
function * g() {
const handle = acquireFileHandle(); // 关键资源
try {
...
}
finally {
handle.release(); // 清理
}
}
const obj = g();
try {
const r = obj.next();
...
}
finally {
obj.return(); // 调用 `g` 中的 finally 块
}
// 异步生成器
async function * g() {
const handle = acquireStream(); // 关键资源
try {
...
}
finally {
await stream.close(); // 清理
}
}
const obj = g();
try {
const r = await obj.next();
...
}
finally {
await obj.return(); // 调用 `g` 中的 finally 块
}
因此,我们提议采用一种新的语法来简化这种常见模式:
// 同步释放
function * g() {
using handle = acquireFileHandle(); // 块作用域关键资源
} // 清理
{
using obj = g(); // 块作用域声明
const r = obj.next();
} // 调用 `g` 中的 finally 块
// 异步释放
async function * g() {
using stream = acquireStream(); // 块作用域关键资源
...
} // 清理
{
await using obj = g(); // 块作用域声明
const r = await obj.next();
} // 调用 `g` 中的 finally 块
此外,我们提议添加两个可释放容器对象以帮助管理多个资源:
DisposableStack — 一个基于栈的可释放资源容器。
AsyncDisposableStack — 一个基于栈的异步可释放资源容器。
状态
阶段: 3
** Champion:** Ron Buckton (@rbuckton)
最后提出: 2023年3月 (幻灯片 ,
笔记 #1 ,
笔记 #2 )
有关更多信息,请参见 TC39 提案流程。
作者
动机
该提案由多个案例驱动:
先例
定义
- 资源 — 具有特定生命周期的对象,在其生命周期结束时,应执行对生命周期敏感的操作,或应关闭或释放非垃圾回收的引用(例如文件句柄、套接字等)。
- 资源管理 — 释放"资源"的过程,触发任何对生命周期敏感的操作或释放任何相关的非垃圾回收引用。
- 隐式资源管理 — 指由运行时作为垃圾回收的一部分隐式管理"资源"生命周期的系统,例如:
WeakMap 键
WeakSet 值
WeakRef 值
FinalizationRegistry 条目
- 显式资源管理 — 指由用户命令式(通过直接调用
Symbol.dispose 等方法)或声明式(通过 using 等块作用域声明)显式管理"资源"生命周期的系统。
语法
using 声明
// 同步释放的块作用域资源
using x = expr1; // 带局部绑定的资源
using y = expr2, z = expr4; // 多个资源
语法规则
有关最新版语法规则,请参阅 规范文本。
await using 声明
// 异步释放的块作用域资源
await using x = expr1; // 带局部绑定的资源
await using y = expr2, z = expr4; // 多个资源
await using 声明可以出现在以下上下文中:
- 在 Module 的顶层,任何允许 VariableStatement 的位置,只要它不直接嵌套在 CaseClause 或 DefaultClause 内部。
- 在异步函数或异步生成器的主体中,任何允许 VariableStatement 的位置,只要它不直接嵌套在 CaseClause 或 DefaultClause 内部。
- 在
for-of 或 for-await-of 语句的开头。
for-of 和 for-await-of 语句中的 await using
for (await using x of y) ...
for await (await using x of y) ...
您可以在异步上下文的 for-of 或 for-await-of 语句中使用 await using 声明,以显式地将每个迭代值绑定为异步可释放资源。for-await-of 不会隐式地将非异步的 using 声明转换为异步的 await using 声明,因为 for-await-of 和 await using 中的 await 标记是针对不同情况的显式指示符:for await 仅表示异步迭代,而 await using 仅表示异步释放。例如:
// 同步迭代,同步释放
for (using x of y) ; // 每次迭代结束时没有隐式 `await`
// 同步迭代,异步释放
for (await using x of y) ; // 每次迭代结束时有隐式 `await`
// 异步迭代,同步释放
for await (using x of y) ; // 每次迭代结束时有隐式 `await`
// 异步迭代,异步释放
for await (await using x of y) ; // 每次迭代结束时有隐式 `await`
虽然后三种情况在执行过程中引入了某种形式的隐式 await,存在一些重叠,但意图是 using 声明中 await 修饰符的存在与否是我们期望迭代值具有 @@asyncDispose 方法的明确指示。这种区别与 for-of 和 for-await-of 的行为一致:
const iter = { [Symbol.iterator]() { return [].values(); } };
const asyncIter = { [Symbol.asyncIterator]() { return [].values(); } };
for (const x of iter) ; // 正常:`iter` 有 @@iterator
for (const x of asyncIter) ; // 抛出异常:`asyncIter` 没有 @@iterator
for await (const x of iter) ; // 正常:`iter` 有 @@iterator (回退)
for await (const x of asyncIter) ; // 正常:`asyncIter` 有 @@asyncIterator
using 和 await using 有相同的区别:
const res = { [Symbol.dispose]() {} };
const asyncRes = { [Symbol.asyncDispose]() {} };
using x = res; // 正常:`res` 有 @@dispose
using x = asyncRes; // 抛出异常:`asyncRes` 没有 @@dispose
await using x = res; // 正常:`res` 有 @@dispose (回退)
await using x = asyncres; // 正常:`asyncRes` 有 @@asyncDispose
这将产生一个基于每个 await 标记存在与否的行为矩阵:
const res = { [Symbol.dispose]() {} };
const asyncRes = { [Symbol.asyncDispose]() {} };
const iter = { [Symbol.iterator]() { return [res, asyncRes].values(); } };
const asyncIter = { [Symbol.asyncIterator]() { return [res, asyncRes].values(); } };
for (using x of iter) ;
// 同步迭代,同步释放
// - `iter` 有 @@iterator: 正常
// - `res` 有 @@dispose: 正常
// - `asyncRes` 没有 @@dispose: *错误*
for (using x of asyncIter) ;
// 同步迭代,同步释放
// - `asyncIter` 没有 @@iterator: *错误*
for (await using x of iter) ;
// 同步迭代,异步释放
// - `iter` 有 @@iterator: 正常
// - `res` 有 @@dispose (回退): 正常
// - `asyncRes` 有 @@asyncDispose: 正常
for (await using x of asyncIter) ;
// 同步迭代,异步释放
// - `asyncIter` 没有 @@iterator: 错误
for await (using x of iter) ;
// 异步迭代,同步释放
// - `iter` 有 @@iterator (回退): 正常
// - `res` 有 @@dispose: 正常
// - `asyncRes` 没有 @@dispose: 错误
for await (using x of asyncIter) ;
// 异步迭代,同步释放
// - `asyncIter` 有 @@asyncIterator: 正常
// - `res` 有 @@dispose: 正常
// - `asyncRes` 没有 @@dispose: 错误
for await (await using x of iter) ;
// 异步迭代,异步释放
// - `iter` 有 @@iterator (回退): 正常
// - `res` 有 @@dispose (回退): 正常
// - `asyncRes` 没有 @@asyncDispose: 正常
for await (await using x of asyncIter) ;
// 异步迭代,异步释放
// - `asyncIter` 有 @@asyncIterator: 正常
// - `res` 有 @@dispose (回退): 正常
// - `asyncRes` 没有 @@asyncDispose: 正常
或者,以表格形式:
语义
using 声明
具有显式局部绑定的 using 声明
UsingDeclaration :
`using` BindingList `;`
LexicalBinding :
BindingIdentifier Initializer
当使用 BindingIdentifier Initializer 解析 using 声明时,声明中创建的绑定将在包含的 Block 或 Module 结束时被跟踪以进行释放(using 声明不能用于 Script 的顶层):
{
... // (1)
using x = expr1;
... // (2)
}
上述示例具有与以下转置表示类似的运行时语义:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
const $$dispose = x[Symbol.dispose];
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
$$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
如果在 using 声明后的块中以及调用 [Symbol.dispose]() 时都抛出异常,将报告所有异常。
具有多个资源的 using 声明
using 声明可以在同一声明中混合多个显式绑定:
{
...
using x = expr1, y = expr2;
...
}
当 Block 或 Module 退出时,这些绑定再次用于执行资源释放,但在这种情况下,[Symbol.dispose]() 将按照其声明顺序的相反顺序调用。这大约等同于以下内容:
{
... // (1)
using x = expr1;
using y = expr2;
... // (2)
}
以上两种情况都具有与以下转置表示类似的运行时语义:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
const $$dispose = x[Symbol.dispose];
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
const y = expr2;
if (y !== null && y !== undefined) {
const $$dispose = y[Symbol.dispose];
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: y, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
$$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
由于我们必须始终确保正确释放资源,因此必须确保在绑定初始化期间可能发生的任何突然完成都会导致清理步骤的执行。当列表中有多个声明时,我们按照声明的顺序跟踪每个资源。因此,我们必须以相反的顺序释放这些资源。
using 声明和 null 或 undefined 值
该提案选择忽略提供给 using 声明的 null 和 undefined 值。这类似于 C# 中 using 的行为,它也允许 null。这样做的一个主要原因是简化资源可能为可选的情况,而无需重复工作或不必要的分配:
if (isResourceAvailable()) {
using resource = getResource();
... // (1)
resource.doSomething()
... // (2)
}
else {
// 上面的重复代码路径
... // 上面的 (1)
... // 上面的 (2)
}
与以下内容相比:
using resource = isResourceAvailable() ? getResource() : undefined;
... // (1) 无论是否有资源都做一些工作
resource?.doSomething();
... // (2) 无论是否有资源都做其他工作
using 声明和没有 [Symbol.dispose] 的值
如果资源没有可调用的 [Symbol.dispose] 成员,则在跟踪资源时会立即抛出 TypeError。
for-of 和 for-await-of 循环中的 using 声明
using 声明可能出现在 for-of 或 for-await-of 循环的 ForDeclaration 中:
for (using x of iterateResources()) {
// 使用 x
}
在这种情况下,每次迭代中绑定到 x 的值将在每次迭代结束时被_同步_释放。这不会释放未迭代的资源,例如,如果迭代因 return、break 或 throw 而提前终止。
using 声明不得在 for-in 循环的开头使用。
await using 声明
具有显式局部绑定的 await using 声明
UsingDeclaration :
`await` `using` BindingList `;`
LexicalBinding :
BindingIdentifier Initializer
当使用 BindingIdentifier Initializer 解析 await using 声明时,声明中创建的绑定将在包含的异步函数体、Block 或 Module 结束时被跟踪以进行释放:
{
... // (1)
await using x = expr1;
... // (2)
}
上述示例具有与以下转置表示类似的运行时语义:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
let $$dispose = x[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = x[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
await $$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
如果在 await using 声明后的语句中以及调用 [Symbol.asyncDispose]() 时都抛出异常,将报告所有异常。
具有多个资源的 await using 声明
await using 声明可以在同一声明中混合多个显式绑定:
{
...
await using x = expr1, y = expr2;
...
}
当 Block 或 Module 退出时,这些绑定再次用于执行资源释放,但在这种情况下,每个资源的 [Symbol.asyncDispose]() 将按照其声明顺序的相反顺序调用。这大约等同于以下内容:
{
... // (1)
await using x = expr1;
await using y = expr2;
... // (2)
}
以上两种情况都具有与以下转置表示类似的运行时语义:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
let $$dispose = x[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = x[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
const y = expr2;
if (y !== null && y !== undefined) {
let $$dispose = y[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = y[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: y, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
await $$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
由于我们必须始终确保正确释放资源,因此必须确保在绑定初始化期间可能发生的任何突然完成都会导致清理步骤的执行。当列表中有多个声明时,我们按照声明的顺序跟踪每个资源。因此,我们必须以相反的顺序释放这些资源。
await using 声明和 null 或 undefined 值
该提案选择忽略提供给 await using 声明的 null 和 undefined 值。这与本提案中 using 声明的建议行为一致。与同步情况一样,这允许简化资源可能为可选的情况,而无需重复工作或不必要的分配:
if (isResourceAvailable()) {
await using resource = getResource();
... // (1)
resource.doSomething()
... // (2)
}
else {
// 上面的重复代码路径
... // 上面的 (1)
... // 上面的 (2)
}
与以下内容相比:
await using resource = isResourceAvailable() ? getResource() : undefined;
... // (1) 无论是否有资源都做一些工作
resource?.doSomething();
... // (2) 无论是否有资源都做其他工作
await using 声明和没有 [Symbol.asyncDispose] 或 [Symbol.dispose] 的值
如果资源没有可调用的 [Symbol.asyncDispose] 或 [Symbol.dispose] 成员,则在跟踪资源时会立即抛出 TypeError。
for-of 和 for-await-of 循环中的 await using 声明
await using 声明可能出现在 for-await-of 循环的 ForDeclaration 中:
for await (await using x of iterateResources()) {
// 使用 x
}
在这种情况下,每次迭代中绑定到 x 的值将在每次迭代结束时被_异步_释放。这不会释放未迭代的资源,例如,如果迭代因 return、break 或 throw 而提前终止。
await using 声明不得在 for-of 或 for-in 循环的开头使用。
隐式异步交错点("隐式 await")
await using 语法在控制流退出包含 await using 声明的异步函数体、Block 或 Module 时引入了一个隐式的异步交错点(即隐式的 await)。这意味着当前在同一微任务中执行的两个语句,例如:
async function f() {
{
a();
} // 退出块
b(); // 与调用 `a()` 在同一微任务
}
如果引入了 await using 声明,则将在不同的微任务中执行:
async function f() {
{
await using x = ...;
a();
} // 退出块,隐式 `await`
b(); // 与调用 `a()` 不同的微任务
}
重要的是,这种隐式交错点应在语法中充分指示。我们认为,在这样的块中存在 await using 是一个足够的指示符,因为在格式良好的代码中,识别包含 await using 语句的 Block 应该相当容易。
编辑器也可以使用语法高亮、编辑器装饰和内嵌提示等功能来进一步突出此类转换,而无需指定额外的语法。
关于 await using 语法及其与隐式异步交错点关系的进一步讨论,请参见 #1。
示例
以下显示了在各种 API 中使用此提案的示例,前提是这些 API 采用了此提案。
WHATWG 流 API
{
using reader = stream.getReader();
const { value, done } = reader.read();
} // 'reader' 被释放
NodeJS FileHandle
{
using f1 = await fs.promises.open(s1, constants.O_RDONLY),
f2 = await fs.promises.open(s2, constants.O_WRONLY);
const buffer = Buffer.alloc(4092);
const { bytesRead } = await f1.read(buffer);
await f2.write(buffer, 0, bytesRead);
} // 先释放 'f2',再释放 'f1'
NodeJS 流
{
await using writable = ...;
writable.write(...);
} // 调用 'writable.end()' 并等待其结果
日志和跟踪
// 审计特权函数调用的进入和退出
function privilegedActivity() {
using activity = auditLog.startActivity("privilegedActivity"); // 记录活动开始
...
} // 记录活动结束
异步协调
import { Semaphore } from "...";
const sem = new Semaphore(1); // 一次只允许一个参与者
export async function tryUpdate(record) {
using lck = await sem.wait(); // 异步阻塞,直到我们成为唯一参与者
...
} // 同步释放信号量并通知下一个参与者
三阶段提交事务
// 如果任一操作失败,则回滚事务
async function transfer(account1, account2) {
await using tx = transactionManager.startTransaction(account1, account2);
await account1.debit(amount);
await account2.credit(amount);
// 如果我们到达此处,则标记事务成功
tx.succeeded = true;
} // 等待事务提交或回滚
共享结构体
main_thread.js
// main_thread.js
shared struct Data {
mut;
cv;
ready = 0;
processed = 0;
// ...
}
const data = Data();
data.mut = Atomics.Mutex();
data.cv = Atomics.ConditionVariable();
// 启动两个 worker
startWorker1(data);
startWorker2(data);
worker1.js
const data = ...;
const { mut, cv } = data;
{
// 锁定互斥量
using lck = Atomics.Mutex.lock(mut);
// 注意:此时我们拥有锁
// 将内容加载到 data 中并发出就绪信号
// ...
Atomics.store(data, "ready", 1);
} // 释放互斥量
// 注意:此时我们不再拥有锁
// 通知 worker 2 应该唤醒
Atomics.ConditionVariable.notifyOne(cv);
{
// 重新获取互斥量上的锁
using lck = Atomics.Mutex.lock(mut);
// 注意:此时我们拥有锁
// 释放互斥量并等待条件满足以重新获取它
Atomics.ConditionVariable.wait(mut, () => Atomics.load(data, "processed") === 1);
// 注意:此时我们拥有锁
// 对处理后的数据做一些操作
// ...
} // 释放互斥量
// 注意:此时我们不再拥有锁
worker2.js
const data = ...;
const { mut, cv } = data;
{
// 锁定互斥量
using lck = Atomics.Mutex.lock(mut);
// 注意:此时我们拥有锁
// 释放互斥量并等待条件满足以重新获取它
Atomics.ConditionVariable.wait(mut, () => Atomics.load(data, "ready") === 1);
// 注意:此时我们拥有锁
// 从 data 中读取值,执行我们的处理,然后指示我们已完成
// ...
Atomics.store(data, "processed", 1);
} // 释放互斥量
// 注意:此时我们不再拥有锁
API
对 Symbol 的补充
该提案向 Symbol 构造函数添加了 dispose 和 asyncDispose 属性,其值为 @@dispose 和 @@asyncDispose 内部符号:
众所周知的符号
TypeScript 定义
interface SymbolConstructor {
readonly asyncDispose: unique symbol;
readonly dispose: unique symbol;
}
SuppressedError 错误
如果在资源释放期间发生异常,则它可能会抑制从主体抛出的现有异常,或者抑制另一个资源的释放。像 Java 这样的语言允许您通过异常的 getSuppressed() 方法访问被抑制的异常。但是,ECMAScript 允许您抛出任何值,而不仅仅是 Error,因此没有方便的地方来附加被抑制的异常。为了更好地呈现这些被抑制的异常并支持日志记录和错误恢复,本提案旨在引入一个新的 SuppressedError 内置 Error 子类,它将包含最近抛出的错误以及被抑制的错误:
class SuppressedError extends Error {
/**
* 包装一个抑制另一个错误的错误,以及被抑制的错误。
* @param {*} error 导致抑制的错误。
* @param {*} suppressed 被抑制的错误。
* @param {string} message 错误消息。
* @param {{ cause?: * }} [options] 错误的选项。
*/
constructor(error, suppressed, message, options);
/**
* 错误的名称(即 `"SuppressedError"`)。
* @type {string}
*/
name = "SuppressedError";
/**
* 导致抑制的错误。
* @type {*}
*/
error;
/**
* 被抑制的错误。
* @type {*}
*/
suppressed;
/**
* 错误消息。
* @type {*}
*/
message;
}
我们选择使用 SuppressedError 而不是 AggregateError 有几个原因:
AggregateError 旨在保存多个错误的列表,这些错误之间没有关联,而 SuppressedError 旨在保存两个具有直接关联的错误的引用。
AggregateError 理想情况下旨在保存一个扁平的错误列表。SuppressedError 旨在保存一组不规则的错误(即,如果存在连续的抑制错误,则为 e.suppressed.suppressed.suppressed)。
AggregateError 上唯一的错误关联是通过 cause,但 SuppressedError 不是由它抑制的错误"导致"的。此外,cause 旨在是可选的,而 SuppressedError 的 error 必须始终被定义。
内置可释放对象
%IteratorPrototype%.@@dispose()
我们还提议将 Symbol.dispose 添加到内置的 %IteratorPrototype% 中,就好像它具有以下行为一样:
%IteratorPrototype%[Symbol.dispose] = function () {
this.return();
}
%AsyncIteratorPrototype%.@@asyncDispose()
我们提议将 Symbol.asyncDispose 添加到内置的 %AsyncIteratorPrototype% 中,就好像它具有以下行为一样:
%AsyncIteratorPrototype%[Symbol.asyncDispose] = async function () {
await this.return();
}
其他可能性
我们还可以考虑将 Symbol.dispose 添加到诸如 Proxy.revocable() 的返回值之类的对象上,但这目前不在当前提案的范围内。
通用 Disposable 和 AsyncDisposable 接口
Disposable 接口
如果对象符合以下接口,则该对象是_可释放的_:
TypeScript 定义
interface Disposable {
/**
* 释放此对象内的资源。
*/
[Symbol.dispose](): void;
}
AsyncDisposable 接口
如果对象符合以下接口,则该对象是_异步可释放的_:
TypeScript 定义
interface AsyncDisposable {
/**
* 释放此对象内的资源。
*/
[Symbol.asyncDispose](): Promise<void>;
}
DisposableStack 和 AsyncDisposableStack 容器对象
此提案添加了两个全局对象,它们可以充当聚合可释放对象的容器,保证在调用相应的释放方法时释放容器中的每个可释放资源。如果容器中的任何可释放对象在释放期间抛出错误,则会在最后抛出(如果抛出了多个错误,则可能会包装在 SuppressedError 中):
class DisposableStack {
constructor();
/**
* 获取一个值,指示堆栈是否已被释放。
* @returns {boolean}
*/
get disposed();
/**
* `[Symbol.dispose]()` 的别名。
*/
dispose();
/**
* 将资源添加到堆栈顶部。如果提供了 `null` 或 `undefined`,则无效果。
* @template {Disposable | null | undefined} T
* @param {T} value - 一个 `Disposable` 对象、`null` 或 `undefined`。
* @returns {T} 提供的值。
*/
use(value);
/**
* 将非可释放资源和释放回调添加到堆栈顶部。
* @template T
* @param {T} value - 要释放的资源。
* @param {(value: T) => void} onDispose - 用于释放提供的值的回调。
* @returns {T} 提供的值。
*/
adopt(value, onDispose);
/**
* 将释放回调添加到堆栈顶部。
* @param {() => void} onDispose - 释放此对象时要评估的回调。
* @returns {void}
*/
defer(onDispose);
/**
* 将当前堆栈中的所有资源移动到一个新的 `DisposableStack` 中。
* @returns {DisposableStack} 新的 `DisposableStack`。
*/
move();
/**
* 释放此对象内的资源。
* @returns {void}
*/
[Symbol.dispose]();
[Symbol.toStringTag];
}
AsyncDisposableStack 是 DisposableStack 的异步版本,是一个用于聚合异步可释放对象的容器,保证在调用相应的释放方法时释放容器中的每个可释放资源。如果容器中的任何可释放对象在释放期间抛出错误,或导致 Promise 被拒绝,则会在最后抛出(如果抛出了多个错误,则可能会包装在 SuppressedError 中):
这些类提供了以下能力:
注意: DisposableStack 的灵感来自 Python 的 ExitStack。
注意: AsyncDisposableStack 的灵感来自 Python 的 AsyncExitStack。
聚合
DisposableStack 和 AsyncDisposableStack 类提供了将多个可释放资源聚合到单个容器中的能力。当 DisposableStack 容器被释放时,容器中的每个对象也保证被释放(除非程序提前终止)。如果任何资源在释放期间抛出错误,它将被收集并在所有资源释放后重新抛出。如果存在多个错误,它们将被包装在嵌套的 SuppressedError 对象中。
例如:
// 同步
const stack = new DisposableStack();
const resource1 = stack.use(getResource1());
const resource2 = stack.use(getResource2());
const resource3 = stack.use(getResource3());
stack[Symbol.dispose](); // 释放 resource3,然后 resource2,然后 resource1
// 异步
const stack = new AsyncDisposableStack();
const resource1 = stack.use(getResource1());
const resource2 = stack.use(getResource2());
const resource3 = stack.use(getResource3());
await stack[Symbol.asyncDispose](); // 释放并等待释放 resource3,然后 resource2,然后 resource1 的结果
如果 resource1、resource2 和 resource3 都在释放时抛出异常,这将产生类似于以下的异常:
new SuppressedError(
/*error*/ exception_from_resource3_disposal,
/*suppressed*/ new SuppressedError(
/*error*/ exception_from_resource2_disposal,
/*suppressed*/ exception_from_resource1_disposal
)
)
互操作和自定义
DisposableStack 和 AsyncDisposableStack 类还提供了从简单回调创建可释放资源的能力。此回调将在执行堆栈的释放方法时执行。
从回调创建可释放资源的能力有几个好处:
- 它允许开发人员在使用不符合
Symbol.dispose/Symbol.asyncDispose 机制的现有资源时利用 using/await using:
{
using stack = new DisposableStack();
const reader = stack.adopt(createReader(), reader => reader.releaseLock());
...
}
- 它赋予用户在块结束时安排其他清理工作的能力,类似于 Go 的
defer 语句:
function f() {
using stack = new DisposableStack();
console.log("enter");
stack.defer(() => console.log("exit"));
...
}
协助复杂构建
用户定义的可释放类可能需要分配和跟踪多个嵌套资源,这些资源应在类实例被释放时释放。但是,在类构造函数中正确管理这些嵌套资源的生命周期有时可能很困难。DisposableStack/AsyncDisposableStack 的 move 方法有助于在这些场景中更轻松地管理生命周期:
// 同步
class PluginHost {
#disposed = false;
#disposables;
#channel;
#socket;
constructor() {
// 创建一个在构造函数退出时释放的 DisposableStack。
// 如果构造成功,我们将所有内容从 `stack` 移出并移入
// `#disposables` 以供稍后释放。
using stack = new DisposableStack();
// 在 process.send/process.on("message") 周围创建一个 IPC 适配器。
// 释放时,它会取消订阅 process.on("message")。
this.#channel = stack.use(new NodeProcessIpcChannelAdapter(process));
// 创建一个伪 websocket,通过 NodeJS IPC 通道发送和接收消息。
this.#socket = stack.use(new NodePluginHostIpcSocket(this.#channel));
// 如果我们到达这里,则构造期间没有错误,并且
// 我们可以安全地将可释放对象从 `stack` 中移出并移入 `#disposables`。
this.#disposables = stack.move();
// 如果构造失败,则在到达上面一行之前,`stack` 将被释放。
// 事件处理程序将被移除,允许 `#channel` 和
// `#socket` 被 GC。
}
loadPlugin(file) {
// 可释放对象应该尝试确保访问与其"已释放"状态一致,尽管这不是严格
// 必要的,因为某些可释放对象可能是可重用的(即,具有 `open()` 方法的 Connection 等)。
if (this.#disposed) throw new ReferenceError("Object is disposed.");
// ...
}
[Symbol.dispose]() {
if (!this.#disposed) {
this.#disposed = true;
const disposables = this.#disposables;
// 注意:我们可以在这里释放 `#socket` 和 `#channel`,因为它们将由下面调用
// `disposables[Symbol.dispose]()` 释放。这不是每个 Disposable 的严格要求,但是
// 良好的内务管理,因为这些对象将不再可用。
this.#socket = undefined;
this.#channel = undefined;
this.#disposables = undefined;
// 释放 `disposables` 中的所有资源
disposables[Symbol.dispose]();
}
}
}
// 异步
const privateConstructorSentinel = {};
class AsyncPluginHost {
#disposed = false;
#disposables;
#channel;
#socket;
/** @private */
constructor(arg) {
if (arg !== privateConstructorSentinel) throw new TypeError("Use AsyncPluginHost.create() instead");
}
// 注意:没有异步构造函数这种东西
static async create() {
const host = new AsyncPluginHost(privateConstructorSentinel);
// 创建一个在构造函数退出时异步释放的 AsyncDisposableStack。
// 如果构造成功,我们将所有内容从 `stack` 移出并移入
// `#disposables` 以供稍后释放。
await using stack = new AsyncDisposableStack();
// 在 process.send/process.on("message") 周围创建一个 IPC 适配器。
// 释放时,它会取消订阅 process.on("message")。
host.#channel = stack.use(new NodeProcessIpcChannelAdapter(process));
// 创建一个伪 websocket,通过 NodeJS IPC 通道发送和接收消息。
host.#socket = stack.use(new NodePluginHostIpcSocket(host.#channel));
// 如果我们到达这里,则构造期间没有错误,并且
// 我们可以安全地将可释放对象从 `stack` 中移出并移入 `#disposables`。
host.#disposables = stack.move();
// 如果构造失败,则在到达上面一行之前,`stack` 将被异步释放。
// 事件处理程序将被移除,允许 `#channel` 和
// `#socket` 被 GC。
return host;
}
loadPlugin(file) {
// 可释放对象应该尝试确保访问与其"已释放"状态一致,尽管这不是严格
// 必要的,因为某些可释放对象可能是可重用的(即,具有 `open()` 方法的 Connection 等)。
if (this.#disposed) throw new ReferenceError("Object is disposed.");
// ...
}
async [Symbol.asyncDispose]() {
if (!this.#disposed) {
this.#disposed = true;
const disposables = this.#disposables;
// 注意:我们可以在这里释放 `#socket` 和 `#channel`,因为它们将由下面调用
// `disposables[Symbol.asyncDispose]()` 释放。这不是每个可释放对象的严格要求,但是
// 良好的内务管理,因为这些对象将不再可用。
this.#socket = undefined;
this.#channel = undefined;
this.#disposables = undefined;
// 释放 `disposables` 中的所有资源
await disposables[Symbol.asyncDispose]();
}
}
}
子类化 Disposable 类
您还可以使用 DisposableStack 来帮助在超类可释放的子类构造函数中进行释放:
class DerivedPluginHost extends PluginHost {
constructor() {
super();
// 创建一个 DisposableStack 来覆盖子类构造函数。
using stack = new DisposableStack();
// 延迟一个回调以释放超类上的资源。我们使用 `defer`,以便调用
// 超类上的 `[Symbol.dispose]` 版本,而不是 this 或任何子类上的版本。
stack.defer(() => super[Symbol.dispose]());
// 如果在子类构造期间任何操作抛出异常,实例仍将被释放,并且超类
// 资源将被释放
doSomethingThatCouldPotentiallyThrow();
// 作为退出前的最后一步,清空 DisposableStack,这样我们就不会释放自己。
stack.move();
}
}
在这里,我们可以使用 stack 来跟踪 super() 的结果(即 this 值)。如果在子类构造期间发生任何异常,我们可以确保调用 [Symbol.dispose](),释放资源。如果子类还需要跟踪自己的可释放资源,此示例会稍作修改:
class DerivedPluginHostWithOwnDisposables extends PluginHost {
#logger;
#disposables;
constructor() {
super()
// 创建一个 DisposableStack 来覆盖子类构造函数。
using stack = new DisposableStack();
// 延迟一个回调以释放超类上的资源。我们使用 `defer`,以便调用
// 超类上的 `[Symbol.dispose]` 版本,而不是 this 或任何子类上的版本。
stack.defer(() => super[Symbol.dispose]());
// 创建一个使用文件系统的记录器,并将其添加到我们自己的可释放对象中。
this.#logger = stack.use(new FileLogger());
// 如果在子类构造期间任何操作抛出异常,实例仍将被释放,并且超类
// 资源将被释放
doSomethingThatCouldPotentiallyThrow();
// 持久化我们自己的可释放对象。如果在调用 `stack.move()` 之前构造失败,我们自己的可释放对象
// 将在设置之前被释放,然后超类的 `[Symbol.dispose]` 将被调用。
this.#disposables = stack.move();
}
[Symbol.dispose]() {
this.#logger = undefined;
// 释放我们自己的资源以及超类的资源。我们不需要调用 `super[Symbol.dispose]()`,因为
// 那已经被构造函数中的 `stack.defer` 调用跟踪了。
this.#disposables[Symbol.dispose]();
}
}
在此示例中,我们可以简单地将新资源添加到 stack,并将其内容移动到子类实例的 this.#disposables 中。在子类的 [Symbol.dispose]() 方法中,我们不需要调用 super[Symbol.dispose](),因为这已被构造函数中的 stack.defer 调用跟踪。
与 Iterator 和 for..of 的关系
ECMAScript 中的迭代器也通过提供 return 方法来实现"清理"步骤。这意味着 using 声明和 for..of 语句之间有一些相似之处:
// 使用
function f() {
using x = ...;
// 使用 x
} // x 被释放
// for..of
function makeDisposableScope() {
const resources = [];
let state = 0;
return {
next() {
switch (state) {
case 0:
state++;
return {
done: false,
value: {
use(value) {
resources.unshift(value);
return value;
}
}
};
case 1:
state++;
for (const value of resources) {
value?.[Symbol.dispose]();
}
default:
state = -1;
return { done: true };
}
},
return() {
switch (state) {
case 1:
state++;
for (const value of resources) {
value?.[Symbol.dispose]();
}
default:
state = -1;
return { done: true };
}
},
[Symbol.iterator]() { return this; }
}
}
function f() {
for (const { use } of makeDisposableScope()) {
const x = use(...);
// 使用 x
} // x 被释放
}
然而,使用 for..of 作为替代方案有许多缺点:
- 主体中的异常会被可释放对象的异常吞掉。
for..of 暗示迭代,这在阅读代码时可能会令人困惑。
- 将
for..of 和资源管理混为一谈可能会使查找文档、示例、StackOverflow 答案等变得更加困难。
- 像上面这样的
for..of 实现无法控制 use 的作用域,这可能会使生命周期变得混乱:
for (const { use } of ...) {
const x = use(...); // 正常
setImmediate(() => {
const y = use(...); // 错误的生命周期
});
}
- 与
using 相比,样板代码要多得多。
- 强制引入新的块作用域,即使在函数体的顶层也是如此。
- 对
for..of 循环的控制流分析无法推断出明确赋值,因为循环可能包含零个元素:
// 使用
function f1() {
/** @type {string | undefined} */
let x;
{
using y = ...;
x = y.text;
}
x.toString(); // x 被明确赋值
}
// for..of
function f2() {
/** @type {string | undefined} */
let x;
for (const { use } of ...) {
const y = use(...);
x = y.text;
}
x.toString(); // 在静态分析器中可能是一个错误,因为不能保证 `x` 已被赋值。
}
- 如果您需要释放迭代值,则使用
continue 和 break 会更加困难:
// 使用
for (using x of iterable) {
if (!x.ready) continue;
if (x.done) break;
...
}
// for..of
outer: for (const x of iterable) {
for (const { use } of ...) {
use(x);
if (!x.ready) continue outer;
if (!x.done) break outer;
...
}
}
与 DOM API 的关系
此提案不一定需要 HTML DOM 规范的立即支持,因为现有 API 仍然可以通过使用 DisposableStack 或 AsyncDisposableStack 进行调整。但是,有许多 API 可以从此提案中受益,并且应由相关标准机构考虑。以下绝不是完整的列表,主要提供考虑建议。实际实现由相关标准机构自行决定。
AudioContext — @@asyncDispose() 作为 close() 的别名或 wrapper。
- 注意:这里的
close() 是异步的,但与其他对象上的类似同步方法使用相同的名称。
BroadcastChannel — @@dispose() 作为 close() 的别名或 wrapper。
EventSource — @@dispose() 作为 close() 的别名或 wrapper。
FileReader — @@dispose() 作为 abort() 的别名或 wrapper。
IDbTransaction — 如果事务仍处于活动状态,@@dispose() 可以调用 abort():
{
using tx = db.transaction(storeNames);
// ...
if (...) throw new Error();
// ...
tx.commit();
} // 如果未到达显式 tx.commit(),则隐式 tx.abort()
ImageBitmap — @@dispose() 作为 close() 的别名或 wrapper。
IntersectionObserver — @@dispose() 作为 disconnect() 的别名或 wrapper。
MediaKeySession — @@asyncDispose() 作为 close() 的别名或 wrapper。
- 注意:这里的
close() 是异步的,但与其他对象上的类似同步方法使用相同的名称。
MessagePort — @@dispose() 作为 close() 的别名或 wrapper。
MutationObserver — @@dispose() 作为 disconnect() 的别名或 wrapper。
PaymentRequest — 如果付款仍处于活动状态,@@asyncDispose() 可以调用 abort()。
- 注意:这里的
abort() 是异步的,但与其他对象上的类似同步方法使用相同的名称。
PerformanceObserver — @@dispose() 作为 disconnect() 的别名或 wrapper。
PushSubscription — @@asyncDispose() 作为 unsubscribe() 的别名或 wrapper。
ReadableStream — @@asyncDispose() 作为 cancel() 的别名或 wrapper。
ReadableStreamDefaultReader — 要么 @@dispose() 作为 releaseLock() 的别名或 wrapper,要么 @@asyncDispose() 作为 cancel() 的 wrapper(但可能不是两者)。
RTCPeerConnection — @@dispose() 作为 close() 的别名或 wrapper。
RTCRtpTransceiver — @@dispose() 作为 stop() 的别名或 wrapper。
ReadableStreamDefaultController — @@dispose() 作为 close() 的别名或 wrapper。
ReadableStreamDefaultReader — 要么 @@dispose() 作为 releaseLock() 的别名或 wrapper,要么
ResizeObserver — @@dispose() 作为 disconnect() 的别名或 wrapper。
ServiceWorkerRegistration — @@asyncDispose() 作为 unregister() 的 wrapper。
SourceBuffer — @@dispose() 作为 abort() 的 wrapper。
TransformStreamDefaultController — @@dispose() 作为 terminate() 的别名或 wrapper。
WebSocket — @@dispose() 作为 close() 的 wrapper。
Worker — @@dispose() 作为 terminate() 的别名或 wrapper。
WritableStream — @@asyncDispose() 作为 close() 的别名或 wrapper。
- 注意:这里的
close() 是异步的,但与其他对象上的类似同步方法使用相同的名称。
WritableStreamDefaultWriter — 要么 @@dispose() 作为 releaseLock() 的别名或 wrapper,要么 @@asyncDispose() 作为 close() 的 wrapper(但可能不是两者)。
XMLHttpRequest — @@dispose() 作为 abort() 的别名或 wrapper。
此外,可以考虑几个利用此功能的新 API:
EventTarget.prototype.addEventListener(type, listener, { subscription: true }) -> Disposable — 传递给 addEventListener 的一个选项可以返回一个 Disposable,释放时会移除事件监听器。
Performance.prototype.measureBlock(measureName, options) -> Disposable — 将 mark 和 measure 组合成一个块作用域的可释放对象:
function f() {
using measure = performance.measureBlock("f"); // 在进入时标记
// ...
} // 在退出时标记和测量
SVGSVGElement — 一个新方法,为 pauseAnimations() 和 unpauseAnimations() 生成一个 single-use disposer。
ScreenOrientation — 一个新方法,为 lock() 和 unlock() 生成一个 single-use disposer。
定义
x() 的 wrapper 是一个调用 x() 的方法,但仅当对象处于调用 x() 不会因重复评估而抛出的状态时。
callback-adapting wrapper 是一个 wrapper,它使接受回调的延续传递风格方法适应为产生 Promise 的方法。
x() 和 y() 的 single-use disposer 表示一个新构造的可释放对象,它在构造时调用 x(),在第一次释放时调用 y()(如果对象被释放多次,则不执行任何操作)。
与 NodeJS API 的关系
此提案不一定需要 NodeJS 的立即支持,因为现有 API 仍然可以通过使用 DisposableStack 或 AsyncDisposableStack 进行调整。但是,有许多 API 可以从此提案中受益,并且应由 NodeJS 维护者考虑。以下绝不是完整的列表,主要提供考虑建议。实际实现由 NodeJS 维护者自行决定。
- 任何具有
ref() 和 unref() 方法的对象 — 一个新方法或 API,为 ref() 和 unref() 生成一个 single-use disposer。
- 任何具有
cork() 和 uncork() 方法的对象 — 一个新方法或 API,为 cork() 和 uncork() 生成一个 single-use disposer。
async_hooks.AsyncHook — 要么 @@dispose() 作为 disable() 的别名或 wrapper,要么一个新方法,为 enable() 和 disable() 生成一个 single-use disposer。
child_process.ChildProcess — @@dispose() 作为 kill() 的别名或 wrapper。
cluster.Worker — @@dispose() 作为 kill() 的别名或 wrapper。
crypto.Cipher, crypto.Decipher — @@dispose() 作为 final() 的 wrapper。
crypto.Hash, crypto.Hmac — @@dispose() 作为 digest() 的 wrapper。
dns.Resolver, dnsPromises.Resolver — @@dispose() 作为 cancel() 的别名或 wrapper。
domain.Domain — 一个新方法或 API,为 enter() 和 exit() 生成一个 single-use disposer。
events.EventEmitter — 一个新方法或 API,为 on() 和 off() 生成一个 single-use disposer。
fs.promises.FileHandle — @@asyncDispose() 作为 close() 的别名或 wrapper。
fs.Dir — @@asyncDispose() 作为 close() 的别名或 wrapper,@@dispose() 作为 closeSync() 的别名或 wrapper
fs.FSWatcher — @@dispose() 作为 close() 的别名或 wrapper。
http.Agent — @@dispose() 作为 destroy() 的别名或 wrapper。
http.ClientRequest — 要么 @@dispose() 或 @@asyncDispose() 作为 destroy() 的别名或 wrapper。
http.Server — @@asyncDispose() 作为 close() 的 callback-adapting wrapper。
http.ServerResponse — @@asyncDispose() 作为 end() 的 callback-adapting wrapper。
http.IncomingMessage — 要么 @@dispose() 或 @@asyncDispose() 作为 destroy() 的别名或 wrapper。
http.OutgoingMessage — 要么 @@dispose() 或 @@asyncDispose() 作为 destroy() 的别名或 wrapper。
http2.Http2Session — @@asyncDispose() 作为 close() 的 callback-adapting wrapper。
http2.Http2Stream — @@asyncDispose() 作为 close() 的 callback-adapting wrapper。
http2.Http2Server — @@asyncDispose() 作为 close() 的 callback-adapting wrapper。
http2.Http2SecureServer — @@asyncDispose() 作为 close() 的 callback-adapting wrapper。
http2.Http2ServerRequest — 要么 @@dispose() 或 @@asyncDispose() 作为 destroy() 的别名或 wrapper。
http2.Http2ServerResponse — @@asyncDispose() 作为 end() 的 callback-adapting wrapper。
https.Server — @@asyncDispose() 作为 close() 的 callback-adapting wrapper。
inspector — 一个新的 API,为 open() 和 close() 生成一个 single-use disposer。
stream.Writable — 要么 @@dispose() 或 @@asyncDispose() 作为 destroy() 的别名或 wrapper,或者 @@asyncDispose 仅作为 end() 的 callback-adapting wrapper(取决于释放行为是立即丢弃还是刷新任何挂起的写入)。
stream.Readable — 要么 @@dispose() 或 @@asyncDispose() 作为 destroy() 的别名或 wrapper。
- ... 以及
net、readline、tls、udp 和 worker_threads 中的许多其他对象。
会议纪要
TODO
以下是推进 TC39 提案流程 每个阶段的的高级任务列表:
Stage 1 进入标准
Stage 2 进入标准
Stage 3 进入标准
Stage 4 进入标准
实现