JavaScript 的新超能力:显式资源管理
JavaScript's New Superpower: Explicit Resource Management

原始链接: https://v8.dev/features/explicit-resource-management

Explicit Resource Management提案增强了JavaScript,通过`using`和`await using`声明实现了确定性的资源生命周期管理。这些声明会在资源超出作用域时自动调用`[Symbol.dispose]()`或`[Symbol.asyncDispose]()`,确保对同步和异步资源(例如文件句柄和网络连接)进行正确的清理。 该提案引入了`DisposableStack`和`AsyncDisposableStack`来管理多个可释放资源。这些堆栈允许使用`use()`、`adopt()`和`defer()`等方法添加资源或释放回调,并以与添加顺序相反的顺序释放它们,从而正确处理依赖关系。`move()`方法可以将所有权转移到其他堆栈。 新的`SuppressedError`类型解决了释放过程中的错误,防止它们掩盖原始错误。这些特性通过提供对资源释放的细粒度控制,提高了代码的健壮性、性能和可维护性,最终防止了资源泄漏并提高了整体代码质量。此功能已在Chromium 134和V8 v13.8中可用。

Hacker News 最新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 JavaScript 的新超能力:显式资源管理 (v8.dev) 17 分,来自 olalonde,2 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系我们 搜索:

原文

The Explicit Resource Management proposal introduces a deterministic approach to explicitly manage the lifecycle of resources like file handles, network connections, and more. This proposal brings the following additions to the language: the using and await using declarations, which automatically calls dispose method when a resource goes out of scope; [Symbol.dispose]() and [Symbol.asyncDispose]() symbols for cleanup operations; two new global objects DisposableStack and AsyncDisposableStack as containers to aggregate disposable resources; and SuppressedError as a new type of error (contain both the error that was most recently thrown, as well as the error that was suppressed) to address the scenario where an error occurs during the disposal of a resource, and potientially masking an existing error thrown from the body, or from the disposal of another resource. These additions enable developers to write more robust, performant, and maintainable code by providing fine-grained control over resource disposal.

using and await using declarations #

The core of the Explicit Resource Management proposal lies in the using and await using declarations. The using declaration is designed for synchronous resources, ensuring that the [Symbol.dispose]() method of a disposable resource is called when the scope in which it's declared exits. For asynchronous resources, the await using declaration works similarly, but ensures that the [Symbol.asyncDispose]() method is called and the result of this calling is awaited, allowing for asynchronous cleanup operations. This distinction enables developers to reliably manage both synchronous and asynchronous resources, preventing leaks and improving overall code quality. The using and await using keywords can be used inside braces {} (such as blocks, for loops and function bodies), and cannot be used in top-levels.

For example, when working with ReadableStreamDefaultReader, it's crucial to call reader.releaseLock() to unlock the stream and allow it to be used elsewhere. However, error handling introduces a common problem: if an error occurs during the reading process, and you forget to call releaseLock() before the error propagates, the stream remains locked. Let's start with a naive example:

let responsePromise = null;

async function readFile(url) {
if (!responsePromise) {
responsePromise = fetch(url);
}
const response = await responsePromise;
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const processedData = await processData(response);


...
}

async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
let processedData;

while (!done) {
({ done, value } = await reader.read());
if (value) {
...
}
}


reader.releaseLock();

return processedData;
}

readFile('https://example.com/largefile.dat');

So it is crucial for developers to have try...finally block while using streams and put reader.releaseLock() in finally. This pattern ensures that reader.releaseLock() is always called.

async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
let processedData;

try {
while (!done) {
({ done, value } = await reader.read());
if (value) {
...
}
}
} finally {
reader.releaseLock();
}

return processedData;
}

readFile('https://example.com/largefile.dat');

An alternative to write this code is to create a disposable object readerResource, which has the reader (response.body.getReader()) and the [Symbol.dispose]() method that calls this.reader.releaseLock(). The using declaration ensures that readerResource[Symbol.dispose]() is called when the code block exits, and remembering to call releaseLock is no longer needed because the using declaration handles it. Integration of [Symbol.dispose] and [Symbol.asyncDispose] in web APIs like streams may happen in the future, so developers do not have to write the manual wrapper object.

 async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;


using readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
},
};
const { reader } = readerResource;

let done = false;
let value;
let processedData;
while (!done) {
({ done, value } = await reader.read());
if (value) {
...
}
}
return processedData;
}

readFile('https://example.com/largefile.dat');

DisposableStack and AsyncDisposableStack #

To further facilitate managing multiple disposable resources, the proposal introduces DisposableStack and AsyncDisposableStack. These stack-based structures allow developers to group and dispose of multiple resources in a coordinated manner. Resources are added to the stack, and when the stack is disposed, either synchronously or asynchronously, the resources are disposed of in the reverse order they were added, ensuring that any dependencies between them are handled correctly. This simplifies the cleanup process when dealing with complex scenarios involving multiple related resources. Both structures provide methods like use(), adopt(), and defer() to add resources or disposal actions, and a dispose() or asyncDispose() method to trigger the cleanup. DisposableStack and AsyncDisposableStack have [Symbol.dispose]() and [Symbol.asyncDispose](), respectively, so they can be used with using and await using keywords. They offer a robust way to manage the disposal of multiple resources within a defined scope.

Let’s take a look at each method and see an example of it:

use(value) adds a resource to the top of the stack.

{
const readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
console.log('Reader lock released.');
},
};
using stack = new DisposableStack();
stack.use(readerResource);
}

adopt(value, onDispose) adds a non-disposable resource and a disposal callback to the top of the stack.

{
using stack = new DisposableStack();
stack.adopt(
response.body.getReader(), reader = > {
reader.releaseLock();
console.log('Reader lock released.');
});
}

defer(onDispose) adds a disposal callback to the top of the stack. It's useful for adding cleanup actions that don't have an associated resource.

{
using stack = new DisposableStack();
stack.defer(() => console.log("done."));
}

move() moves all resources currently in this stack into a new DisposableStack. This can be useful if you need to transfer ownership of resources to another part of your code.

{
using stack = new DisposableStack();
stack.adopt(
response.body.getReader(), reader = > {
reader.releaseLock();
console.log('Reader lock released.');
});
using newStack = stack.move();
}

dispose() in DisposableStack and asyncDispose() in AsyncDisposableStack dispose the resources within this object.

{
const readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
console.log('Reader lock released.');
},
};
let stack = new DisposableStack();
stack.use(readerResource);
stack.dispose();
}

Availability #

Explicit Resource Management is shipped in Chromium 134 and V8 v13.8.

Explicit Resource Management support #

联系我们 contact @ memedata.com