放弃 try-catch-finally ,试试 JavaScript 的显式资源管理

782 阅读5分钟

什么是显示资源管理?

众所周知,JavaScript 有自动的垃圾回收机制(GC),可以自动的释放内存对象,但对与文件句柄、网络连接等资源的释放却无能为力,需要手动的进行资源的释放。因此,我们需要一种明确的、作用域级别的资源清理机制。

在最新的 ECMAScript 规范中,引入 using 关键字,以及 Symbol.disposeSymbol.asyncDispose 两个 Symbol 属性,用来声明一个资源,并在资源释放时进行一些清理操作。

注意:这个最新的功能仅在 Chromium 134Node 24.0.0 以上的版本支持。

using

显示资源管理的核心是使用 using 关键字声明一个资源,来确保资源在当前作用作用域结束时调用 [Symbol.dispose]() 方法,在这个方法中我们可以进行一些资源的释放操作。

如果是异步资源,则需要使用 await using 用来声明,对应的方法是 [Symbol.asyncDispose]()

我们先来看一下传统的资源释放操作:

import fs from 'fs'

function writeLog() {
  let file
  try {
    file = fs.openSync('log.txt', 'a')
    fs.writeSync(file, '写入一条日志\n')
  } finally {
    if (file) {
      fs.closeSync(file) // 释放资源
      console.log('文件关闭')
    }
  }
}
writeLog()

在上面的代码中,为了避免在读取文件的过程中报错导致文件没有关闭,我们需要使用 try finally对代码进行包裹,确保在 finally 块中释放资源。

而使用 using 关键字可以在函数执行结束之后自动帮我们释放资源:

import fs from 'fs';

function writeLog() {
  // 将一个资源对象赋值给 using 关键字声明的变量
  using fileSource = {
    file: fs.openSync('log.txt', 'a'),
    write(content) {
      fs.writeSync(this.file, content)
    },
    [Symbol.dispose]() {
      fs.closeSync(this.file)
      console.log('文件关闭')
    }
  }
  fileSource.write('写入一条日志\n') // 写入日志
}
writeLog() // 在函数执行完毕后,自动释放资源

在上面的代码中,我们定义了一个对象,对象里面有一个 [Symbol.dispose] 方法,在该方法中释放了文件句柄。

然后,我们使用 using 关键字声明了一个 fileSource 变量,来接收对象,using 关键字声明的变量所在的函数执行结束后,会自动调用 fileSource 身上的 [Symbol.dispose] 方法,释放资源

如果是异步的操作,则需要在 using 关键字前面加一个 await,来声明一个异步资源,然后在类中实现 [Symbol.asyncDispose]() 方法。

DisposableStack 和 AsyncDisposableStack

为了方便管理多个资源,我们可以使用 DisposeableAsyncDisposeable 这两个构造器实例化一个类似栈的结构。

我们可以给这个结构不断添加资源,当函数执行结束时,不管是同步资源还是异步资源,这些资源的处理顺序与它们添加的顺序刚好相反,从而确保它们之间的依赖关系能够得到一个正确的处理。

所以,当处理多个有互相依赖关系的资源时,资源的释放和清理过程就得到了简化。

同样的,Disposeable 用来处理同步资源,AsyncDisposeable 用来处理异步资源。

这个结构身上有 useadoptdefermovedispose 等方法。

接下来讲一下他们的用法:

use

use 函数可以将多个实现了 [Symbol.dispose] 方法的对象添加到 DisposableStack 中, 在函数作用域执行结束后,会自动调用这些对象身上的 [Symbol.dispose] 方法。

添加到 DisposableStack 中的资源,必须是一个“可释放对象”(实现了 [Symbol.dispose] 方法的对象)。

import fs from 'fs'

function writeLog() {
  const fileSource = {
    file: fs.openSync('log.txt', 'a'),
    write(content) {
      fs.writeSync(this.file, content)
    },
    [Symbol.dispose]() {
      fs.closeSync(this.file)
      console.log('文件关闭')
    },
  }

  // 创建一个  Disposeable 实例
  using stack = new DisposableStack()

  stack.use(fileSource) // 将资源添加到 Disposeable 实例中
  // stack.use(fileSource1) // 可以添加多个

  fileSource.write('写入一条日志\n') // 写入日志
}
writeLog() // 在函数执行完毕后,自动释放资源

在上面的代码中,我们创建了一个 Disposeable 实例,然后使用 use 方法将 fileSource 资源添加到实例中。

当函数执行结束时,fileSource 资源会被自动释放,因为 stack 实例在函数执行结束时会依次调所加入资源身上的 [Symbol.dispose] 方法,释放所有资源。释放顺序与它们添加的顺序刚好相反。

adopt

如果一个第三方资源没有实现 [Symbol.dispose] 方法,可以使用 adopt 方法给其注册一个清理函数。

import fs from 'fs'

function writeLog() {
  // 创建一个  Disposeable 实例
  using stack = new DisposableStack()

  // 需要被释放的资源
  const file = fs.openSync('adopted-log.txt', 'a');


  // 注册清理函数
  stack.adopt(file, (file)=>{
      fs.closeSync(file)
      console.log('文件关闭')
  })

  fs.writeSync(file, '写入一条日志\n') // 写入日志

}
writeLog() // 在函数执行完毕后,自动释放资源

在上面的代码中,我们创建了一个 Disposeable 实例,然后使用 adopt 方法给 file 资源注册了一个清理函数。当函数执行结束时,file 资源会被自动释放。

defer

defer 方法可以将一个回调函数添加到栈的顶部,不依赖资源对象或资源返回值,只是做清理任务。

using stack = new DisposableStack()

// 注册清理函数
stack.defer(()=>{
    console.log('一些清理任务')
})

move

move 方法可以将一个资源对象从一个 Disposeable 实例中移动到另一个 Disposeable 实例中。

const stack = new DisposableStack();

const file = {
  [Symbol.dispose]() {
    console.log('清理文件');
  }
};
stack.use(file);           // 加入 stack
using newStack = stack.move() // 将资源从 stack 移动到 newStack

dispose

dispose 方法可以手动调用 Disposeable 实例中的资源清理函数。

const stack = new DisposableStack()

const file = {
  [Symbol.dispose]() {
    console.log('清理文件')
  },
}
stack.use(file) // 加入 stack
stack.dispose() // 手动调用清理函数