Javascript 中的 using、Disposable 和显式资源管理
Javascript 的新“显式资源管理”提案添加了 using
语句,该语句可以在您使用完资源后自动关闭资源。但作为同一提案的一部分,还添加了许多其他 API,使 using
更加有用。当我试图了解这些 API 的工作原理时,我找不到很多相关文档,因此本文是对 using
、Disposable
和显式资源管理的概述。
使用 using
的历程
许多类或对象代表某种资源,例如打开的文件或数据库连接,当这些资源不再使用时需要进行清理逻辑。在 NodeJS 中,通常的惯例是将清理逻辑放在 close()
函数中。例如,node:http
中的 Server
类有一个 close()
方法,用于停止新连接并关闭现有连接。
仅使用 close
的问题在于,很容易忘记调用它。有时是由于疏忽,但更常见的是错误和异常会导致问题。考虑以下函数:

async function saveMessageInDatabase(message: string) {
const conn = new DatabaseConnection();
const { sender, recipient, content } = parseMessage();
await conn.insert({ sender, recipient, content });
await conn.close();
}
该函数在函数开始时创建数据库连接,并在结束时关闭它。但如果 parseMessage
或 conn.insert(...)
抛出错误,就会出现问题——在这种情况下,saveMessageInDatabase
函数会停止运行,而不会关闭连接,导致未关闭的资源悬而未决。
解决此问题的第一步是正式确定资源如何关闭。我们可以使用 Disposable
接口或协议来做到这一点:
interface Disposable {
[Symbol.dispose]: () => void;
}
这与 .close()
、.cancel()
等方法非常相似,但我们使用的是 众所周知的符号 Symbol.dispose
。这有助于运行时区分故意设置为可丢弃的对象(使用此符号)和偶然具有特定名称的对象(例如 door.close()
)。
这样,我们就可以定义 using
语法了。using
的使用方式与 const
非常相似,在大多数方面表现也非常相似。
// was: const varName = new MyDisposableObject();
// now:
using varName = new MyDisposableObject();
varName.method();
然而,除了 const
的正常行为外,using
还有一些限制:
- 使用
using
时,变量必须实现Disposable
协议——即它必须具有一个无需参数即可调用的Symbol.dispose
方法。 - 使用
using
时,您无法解构您感兴趣的变量(例如using { field1, field2 } = disposable()
)。
当我们离开定义 using
变量的范围时(即当我们从使用 using
的函数返回、离开 if 块或抛出异常等),将调用 Symbol.dispose
方法。
此外,处置方法将按照与定义顺序相反的顺序被调用。因此,首先创建的对象将最后被处置。这种相反的顺序是故意的:我们不希望在其他资源仍在使用数据库时就清理数据库!
示例:
function create(data: string) {
console.log(`creating disposable object with data ${data}`);
return {
data: data,
[Symbol.dispose]() {
console.log(`disposing disposable object with data ${data}`);
},
}
}
function main() {
using data1 = create("first");
using data2 = create("second");
console.log(`using ${data1.data} and ${data2.data}`);
}
console.log("before main");
main();
console.log("after main");
这将产生以下日志:
before main
creating disposable object with data first
creating disposable object with data second
using first and second
disposing disposable object with data second
disposing disposable object with data first
after main
异步 Disposables
通常,当我们想要在 Javascript 中处理一个资源时,清理/关闭/处理操作需要异步进行——例如,由于等待连接被清理或数据被刷新。
因此,除了标准的 Disposable
协议外,还有第二个协议 AsyncDisposable
。它使用 Symbol.asyncDispose
方法,该方法返回一个承诺,当资源完全关闭时,该承诺应得到解决。
要使用异步可丢弃对象,我们可以使用 await using
语法,它与普通的 using
声明做相同的事情,但以异步方式进行。它必须在 async
函数内调用(即需要在允许使用普通 await
的地方调用)。
await using
语法还可以处理非异步 Disposable
对象,这意味着,如果您不知道给定资源是异步还是同步处置,可以使用 await using
来涵盖两种情况。
await using |
using |
|
---|---|---|
Usable In: | async functions only | async and sync functions |
Protocol: | AsyncDisposable + Disposable |
only Disposable |
Symbol: | Symbol.asyncDispose /Symbol.dispose |
Symbol.dispose |
Disposables 集合
有时仅使用函数作用域来处理资源是不够的。我们可能有一个类或对象,它拥有并管理多个不同的资源。如果我们能够以类似于 using
的方式将所有资源分组在一起,但作为类变量或闭包的一部分,那将非常有用。
实现这一功能的一种便捷方式是使用 DisposableStack
和 AsyncDisposableStack
类。这些类表示由 DisposableStack
或 AsyncDisposableStack
对象管理的资源栈——如果我们清理栈,它所管理的全部资源也将被清理。这些类可通过 new
运算符创建,并提供三种不同的方法用于向栈中添加资源:
const stack = new DisposableStack(); // or ` = new AsyncDisposableStack()`
// .use() takes a resource, adds it to the resources managed by the stack,
// and returns the resource so it can be used elsewhere.
// The resource must be a disposable item using the `Symbol.dispose` method
// (or `Symbol.asyncDispose` if using `AsyncDisposableStack`).
const resource1 = stack.use(new MyResource());
// `.defer()` schedules a callback to take place when the stack gets disposed.
const handle = setInterval(() => console.log("running"), 5000);
stack.defer(() => clearInterval(handle));
// .adopt() combines the above two behaviours: it takes a resource and a
// disposal callback, and calls the callback with the resource as its argument
// when the stack gets disposed. The resource does _not_ need to implement
// the disposable protocol.
const resource2 = stack.adopt(new OtherResource(), (r) => r.close());
堆栈本身是可丢弃的,因此可以使用 Symbol.dispose
或 Symbol.asyncDispose
方法进行丢弃。为方便起见,此方法也简单地暴露为 .dispose()
或 .asyncDispose()
。如前所述,此方法(无论是使用 Symbol.dispose
/Symbol.asyncDispose
还是通过别名方法)都会处理该堆栈管理的所有资源。与 using
声明类似,资源将按照相反的顺序进行清理,以防止资源相互依赖的问题。
AsyncDisposableStack |
DisposableStack |
|
---|---|---|
Implements: | AsyncDisposable |
Disposable |
Manages: | AsyncDisposable + Disposable |
only Disposable |
Dispose With: | .disposeAsync() |
.dispose() |
显式资源管理的有用模式
希望到现在为止,您已经清楚了可丢弃资源是什么、如何操作它们等。但我仍然花了一些时间来理解一些典型的可丢弃模式、如何操作它们以及一些最佳实践。因此,以下几个简短的部分将解释一些现在可以实现的想法,以及实现一些常见目标的最佳方法。
匿名延迟
如果你来自其他具有类似 using
功能的语言,例如 Go 中的 defer
语句,那么你可能会惊讶地发现 using
实际上是一个变量声明——它基本上与 const
或 let
相同,但它还会在函数完成后自动清理资源。
因此,每次使用 using
时,你都需要声明一个变量名。但很多时候你并不想指定变量名——例如,如果你想将回调推迟到函数结束时执行。你可以使用 using _ = ...
这种写法,将 _
作为一种非正式的占位符,但这种写法在每个函数体内只能使用一次。
我找到的最佳解决方案是 DisposableStack
(或 AsyncDisposableStack
) 和 defer
方法:
function main() {
using stack = new DisposableStack();
console.log("starting function");
stack.defer(() => console.log("ending function"));
console.log("during function body");
}
Disposable Timeouts and Intervals
该规范提供了将采用一次性机制的各种浏览器和 JS API 的列表,但明显缺失的一个 API 是 setInterval
和 setTimeout
函数。目前,它们可以使用 clearInterval
和 clearTimeout
函数取消,我们还可以使用 defer
将它们集成到一次性堆栈中:
function main() {
using stack = new DisposableStack();
const handle = setInterval(() => {/* ... */}, 5000);
stack.defer(() => clearInterval(handle));
// ...
}
事实上,如果你使用 NodeJS,你可以做得更好。NodeJS 的定时器(即 setInterval
及其相关函数)已经返回一个对象(Timeout
),其中包含许多有用的函数,如 .unref()
。v18 及以上版本现在还为这些对象添加了 Symbol.dispose
键,这意味着你可以将上面的代码简化为:
function main() {
using setInterval(() => {/* ... */}, 5000);
// ...
}
在浏览器中,我们可以编写一个类似的实用函数:
export function interval(...args: Parameters<typeof setInterval>): Disposable {
const handle = setInterval(...args);
return { [Symbol.dispose]: () => clearInterval(handle) };
}
使用和移动操作
DisposableStack 工具箱中还有一个微妙但非常强大的方法,即 .move()。它会创建一个新堆栈,将当前堆栈中的所有资源移到新堆栈中,然后将原始堆栈标记为已处理。除了原始堆栈之外,其他资源都不会被处理。
这是一种强大的工具,适用于需要创建资源供其他地方使用(因此希望将它们放入栈中)的场景,但创建资源本身可能失败(因此希望在创建时使用该栈)。
考虑以下示例,它可能导致资源泄漏:
export function badOpenFiles(
fileList: string[]
): { files: File[] } & Disposable {
const stack = new DisposableStack();
const files = [];
for (const fileName of fileList) {
files.push(stack.use(open(fileName)));
}
return {
files,
[Symbol.dispose]: () => stack.dispose(),
};
}
如果我用文件列表调用这个函数,它会打开所有这些文件,并将其作为单个可丢弃对象返回,当该对象被丢弃时,会再次关闭所有文件。
但假设我用文件列表 [“file1.txt”, “file2.txt”, “file-does-not-exist.txt”, “file4.txt”]
调用这个函数。现在,前两个文件会被创建并添加到栈中,但第三个文件会引发错误并退出函数。由于栈从未被添加到任何 using
声明中,这些文件将永远不会被关闭,从而导致资源泄漏。
.move()
函数允许我们这样做:
export function openFiles(fileList: string[]): { files: File[] } & Disposable {
// Note we use the stack here, which means this stack will be cleaned up
// when we return from the function.
using stack = new DisposableStack();
const files = [];
for (const fileName of fileList) {
files.push(stack.use(open(fileName)));
}
// We've successfully created all the files with no errors.
// Move the opened file resources out of the stack, and into a new
// one that won't get closed at the end of this function.
const closer = stack.move();
return {
files,
[Symbol.dispose]: () => closer.dispose()
};
}
Disposable Wrapper
目前,Disposable
还相对较新,因此你正在使用的库可能不支持它们,而是选择使用 .close()
方法或其他类似方法。但 JavaScript 是一种动态语言,因此让这些工具正常工作并不太难。以下是 MongoDB 的示例,在撰写本文时,MongoDB 不支持 using
或任何一次性协议:
function createMongoClient(
connection: string,
options?: MongoClientOptions
): MongoClient & Disposable {
const client = new MongoClient(connection, options);
if (client[Symbol.asyncDispose]) throw new Error("this code is unnecessary");
return Object.assign(client, { [Symbol.asyncDispose]: () => client.close() });
}
这将向客户端添加一个 Symbol.asyncDispose
方法,这意味着您可以在 await using
声明中以及与 AsyncDisposableStack#use()
一起使用它。此外,如果您更新到实现了 AsyncDisposable
协议的 MongoDB 版本,您会收到一个错误,提醒您删除该代码。
等待信号
NodeJS 服务器中常见的模式是启动 Web 服务器,然后为操作系统的“退出”信号(如 SIGINT
)添加事件处理程序。在这些处理程序中,我们可以关闭服务器、清理资源等。
这可以正常工作,但控制流可能难以跟踪,而且我们必须在相关的地方手动调用 Symbol.dispose
和 Symbol.asyncDispose
方法。如果有一种方法可以将应用程序的寿命与单个函数的寿命绑定在一起,那么当该函数退出时,服务器也会自动退出,并关闭和处理所有资源,那就太好了。
进入 NodeJS 的 once
函数,它将事件转换为承诺1:
import { once } from "node:events";
// waits until the `SIGINT` signal has been triggered, e.g. from Ctrl-C
await once(process, "SIGINT");
利用此功能,我们可以编写一个贯穿应用程序整个生命周期的 main()
函数,并在应用程序停止时退出,同时自动清理自身使用的所有资源:
async function main() {
await using stack = new AsyncDisposableStack();
await using resource = createResource();
// ... etc, for as many resources as make sense
// alternatively, use the "Disposable Wrapper" pattern from earlier
const server = stack.adopt(express(), server => server.close());
// add routes, use resources, etc
server.get(/* ... */);
logger.info("starting application on port 5000");
server.listen(5000, () => logger.info("application started"));
await Promise.race([
once(process, "SIGINT"), // Ctrl-C
once(process, "SIGTERM"), // OS/k8s/etc requested termination
// etc.
]);
logger.info("shutting down application");
}
由于所有资源均通过 using
直接管理,或通过与应用程序生命周期绑定的 stack
管理,因此在处理完信号后无需手动关闭任何资源——一切将自动优雅地关闭。
使用显式资源管理
目前,显式资源管理处于第三阶段提案——这意味着规范已经完成并获得批准,鼓励浏览器和其他工具开始实施该提案,并在实践中尝试该功能2。
这意味着,如果你想立即尝试该功能,你需要将语法转换为旧版浏览器支持的格式,并为该功能所需的各种全局对象提供 polyfill。
在转译方面,TypeScript 和 Babel 均支持 using ...
语法。对于 TypeScript,你需要使用 版本 5.2 或更高版本 来处理该语法。在 tsconfig.json
文件中,target
应设置为 ES2022
或更低版本才能进行转译(否则 Typescript 将保持语法不变),并且 lib
设置应包括 esnext.disposable
(或整个 esnext
类型集合),以确保包含正确的类型集。对于 Babel,使用 stage-3
预设,或显式添加 @babel/plugin-proposal-explicit-resource-management
插件 可确保所有内容正确编译。
TypeScript 和 Babel 均未包含本文中讨论的各种全局类型的 polyfill,这意味着你还需要一个 polyfill。为此,有多种选项可供选择,包括我一直在使用的 disposablestack 和 CoreJS。
将所有内容整合在一起
假设我们在后端编写一些内容,并与用户进行了大量交互。我们希望有一个类来处理所有基于用户的逻辑:
- 它可以创建/读取/更新/删除数据库中的用户对象
- 它会自动删除过去五分钟内未活跃的用户(我们非常重视GDPR合规性,不会比必要时间多保留一秒钟的数据)
- 它提供一个流,包含所有新插入数据库的用户,以便我们在其他地方向他们发送友好的欢迎信息。
我们将使用依赖注入,这样就不需要自己创建数据库连接,但其中一些资源将由用户服务进行管理。我们可以使用 AsyncDisposableStack
将这些资源进行分组,并添加一个 Symbol.asyncDispose
方法,该方法委托给堆栈的 dispose 方法来处理资源的释放。我们甚至可以实现 TypeScript 的 AsyncDisposable
接口,以确保一切正确无误:
export class UserService implements AsyncDisposable {
#conn: DB.Conn;
#stack = new AsyncDisposableStack();
#intervalHandle: NodeJS.Timeout;
#streamHandle: DB.StreamHandle;
constructor(conn: DB.Conn) {
// Our DB connection, passed in via dependency injection -- so we don't want
// to add this to the set of resources managed by this service!
this.#conn = conn;
// Remember, NodeJS's `Timeout` class already has a `Symbol.dispose` method,
// so we can add that to the stack
this.#timeoutHandle = this.#stack.use(
setInterval(() => this.deleteInactiveUsers(), 60 * 1000)
);
// For resources that don't have the right methods, `.adopt()` is the
// easiest way to add an "on dispose" cleanup function
this.#streamHandle = this.#stack.adopt(
this.#createNewUserStream(),
// Closing this stream is an async operation, hence why we're using
// Symbol.asyncDispose, AsyncDisposableStack, etc.
async (stream) => await stream.close()
);
}
async [Symbol.asyncDispose]() {
await this.#stack.dispose();
}
// ... methods and implementation details
}
现在,我们需要在某处实际构建所有资源。我们可以添加一个 createResources
函数来创建资源,返回一个可清理所有资源的可抛弃对象,并确保在资源构建过程中发生错误时,一切仍能正常清理——这就是堆栈的强大之处!
export async function createResources(config: Config) {
await using stack = new AsyncDisposableStack();
const db = new MyDbDriver(config.connectionString);
const conn = stack.use(await db.connect());
// When the stack disposes of its resources, the `UserService` will be cleaned
// up before the `conn`, which prevents errors where the `UserService` is
// trying to use a connection that doesn't exist anymore.
const userService = stack.use(new UserService(conn));
// Now all the resources have been set up, use .move() to create a new stack
// and return a dispose method based on the new stack.
const closer = stack.move();
return {
userService,
[Symbol.asyncDispose]: async () => await closer.dispose(),
};
}
最后,我们需要将所有这些内容集成到服务器实现中,并确保在服务器优雅退出时所有资源都能被清理。我们可以使用信号和承诺来捕获应触发关闭的事件,并使用 using
声明自动清理任何资源。
async function main() {
const config = await loadConfig();
using resources = createResources(config);
using stack = new AsyncDisposableStack();
// create wrapper functions around APIs that don't yet support disposables
using server = createFastify({});
server.get(/* route using resources.userService */)
await server.listen({ port: 3000 })
logger.info("server running on port 3000");
// use `once` to turn one-time events into promises
await Promise.race([
once(process, "SIGINT"),
once(process, "SIGTERM"),
once(process, "SIGHUP"),
]);
logger.info("server terminated, closing down");
// resources will all be cleaned up here
}
更多资源
关于显式资源管理提案,还有许多其他信息,但鉴于谷歌已经不像以前那样运作了,以下是一些链接,可以帮助您找到这些信息:
- 原始提案 在 TC39 GitHub 组织中。文档内容较为技术导向,但问题部分提供了大量关于 API 的背景信息,以及多个示例解释了为何做出某些决策。
- TypeScript 5.2的发布说明中包含关于资源管理、如何使用以及如何在TypeScript中实现的专属章节。
- 有关实现显式资源管理的各种问题/错误/提案:
- Chrome
- Firefox
- 其他语言中的先例:
- C# 的
using
语句 对这里的影响很大。 - Go 的
defer
语句 启发了.defer
方法。 - Python 的
ExitStack
是DisposableStack
和AsyncDisposableStack
类的明确灵感来源。
- C# 的
- 需要注意的是,如果你无法访问 NodeJS,你可以手动使用
new Promise(...)
来实现这一功能,尽管这样做可能比较棘手,因为很难确保在事件触发后资源能够正确清理。↩︎ - 对于喜欢这种功能的人来说,你可以查看在 Chrome、 Firefox 和 Safari 中的相关票据。↩︎
你也许感兴趣的:
- JavaScript™ 商标更新
- 关于 JavaScript “工作证明(proof of work) “防抓取系统的思考
- 这是 JavaScript 吗?
- 为什么 2025/05/28 和 2025-05-28 在 JavaScript 中是不同的日子?
- JavaScript 的新超能力:显式资源管理
- 为 V8 提个醒: 通过明确的编译提示加快 JavaScript 启动速度
- 【程序员搞笑图片】纯 JavaScript vs. 框架
- JavaScript 框架选择困难症仍在增加
- 【程序员搞笑图片】盒子里有什么?javascript
- Node.js之父ry“摇人”——要求Oracle放弃JavaScript商标
你对本文的反应是: