Typescript 5.2 Beta发布,带来全新的using声明

这些年typescript发展得如火如荼,从它的发展路线来看,它不单单弥补了js在类型方面的缺陷,还超前带来许多新的语言特性。今年6月底,typescript 5.2 Beta发布,其中带来了很重要的一个特性:using声明,方便我们对资源进行管理。

背景

编程的世界需要管理很多资源,这些资源使用完之后需要及时释放,比如数据库操作完之后断开连接、文件open并操作完后要close掉、线程占用之后要及时解开线程锁等等。

就拿我们最熟悉的nodejs来说,fs.openfs.close总是成对出现,fs.open打开文件,方便执行多次操作,fs.close最后关闭文件,释放资源。比如下面的例子:

javascript 复制代码
import fs from 'node:fs/promises';

async function doSomething() {
  const path = ".some_temp_file";
  const file = await fs.open(path, "w+");

  // use file...
  if (someCondition()) {
    // do some more work...
    methodA();
    const result = methodB();
    methodC();

    // Close the file and delete it.
    await fs.close(file);
    await fs.unlink(path);
    
    return result;
  }

  methodD();
  // Close the file and delete it.
  await fs.close(file);
  await fs.unlink(path);
}

这段代码有什么问题?能用,但不够优雅,因为它在不同的位置重复写了"清理"代码(fs.closefs.unlink)。该如何优化?目前似乎只能使用try/finally

javascript 复制代码
import fs from 'node:fs/promises';

async function doSomething() {
  const path = ".some_temp_file";
  const file = await fs.open(path, "w+");

  try {
    // use file...
    if (someCondition()) {
      // do some more work...
      methodA();
      const result = methodB();
      methodC();

      return result;
    }

    methodD();
  } finally {
    // Close the file and delete it.
    await fs.close(file);
    await fs.unlink(path);
  }
}

现在好了一些,但依然不是最好的方案。

为了保证离开作用域时释放资源,我们使用了try/finally,代码层级增加。如果有多个需要释放的资源,加上资源在释放过程会抛出错误,我们可能需要更深层次的try/finally语句的包装。这有点像回调地域,代码的可读性和可维护性直线下降。

另外,上面没有考虑到有依赖关系的资源的释放顺序,以及资源释放过程中错误的处理。

using声明

using关键字是除了varletconst以及全局声明之外的变量声明的方式。基本用法如下:

typescript 复制代码
import fs from 'node:fs';

class TempFile implements Disposable {
  #path: string;
  #handle: number;

  constructor(path: string) {
    this.#path = path;
    this.#handle = fs.openSync(path, 'w+');
  }

  [Symbol.dispose]() {
    // Close the file and delete it.
    fs.closeSync(this.#handle);
    fs.unlinkSync(this.#path);
  }
}

function doSomething() {
  using file = new TempFile(".some_temp_file");
}

它声明的变量是一个对象,这个对象上需要有[Symbol.dispose]()方法,定义具体如何释放资源。using声明代表着,当离开变量所在作用域时,程序会自动执行变量的[Symbol.dispose]()方法。

Symbol.dispose是新标准提出的Symbol上的一个新属性,用于资源释放。

using声明带来这些好处:

  1. 避免资源释放相关代码的重复调用,保证离开作用域之前执行;
  2. 避免try/finally的使用以及嵌套;
  3. 统一实现,目前ECMAScript Iterators、WHATWG Stream Readers、NodeJS FileHandles的资源释放方法都不一样,新标准出来之后,这些实现有望达成统一,那样就不用再手动封装[Symbol.dispose]()方法了;
  4. 对于读写锁、IO操作等十分有用;
  5. 配合shared struct(一个新的提议)使用。

using声明的实现可以用下面的代码表示:

javascript 复制代码
{
  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;
    }
  }
}

这段代码可以解释以下这些特性。

必须要有Symbol.dispose

使用using关键字声明的对象必须有[Symbol.dispose]()方法,否则抛出TypeError

资源释放顺序

使用using声明的变量,资源释放顺序遵循栈FILO的逻辑。也就是说,下面代码中,在离开run函数的作用域之前,b[Symbol.dispose]()先于a[Symbol.dispose]()执行。

javascript 复制代码
function run() {
  using a = funcA();
  using b = funcB();
  // b[Symbol.dispose]();
  // a[Symbol.dispose]();
}

为什么这么设计?因为资源b有可能依赖于资源a,如果a先释放,则b的释放会出错。

错误处理

无论是执行主体代码还是资源释放过程都可能抛出错误,如果主体代码抛出错误,比如下面:

javascript 复制代码
function run() {
  function doSomething() {
    using a = funcA();
    throw new Error('主体代码错误');
    // dispose a
  }
  
  try {
    doSomething();
  } catch (e) {
    console.error(e);
  }
}

资源a是否就无法释放了?

另外,如果资源a在释放过程中抛出错误,这个错误会覆盖主体错误吗?如何用一个error变量保留多个错误信息?

为此,标准引入了一个新的错误类型SuppressedError,它继承于Error对象。它的实现很简单,看一下tsc的编译结果:

javascript 复制代码
function SuppressedError(error/* 资源释放错误 */, suppressed/* 主体错误 */, message) {
  const e = new Error(message);
  e.name = 'SuppressedError';
  e.error = error;
  e.suppressed = suppressed;
  return e;
}

错误对象结构如下:

javascript 复制代码
const e = {
  message: 'An error was suppressed during disposal.',
  name: 'SuppressedError',
  error: {
    message: '资源释放错误',
    name: 'Error',
    stack: '...',
  },
  suppressed: {
    message: '主体错误',
    name: 'Error',
    stack: '...',
  },
  stack: '...',
}

值得注意的是,资源释放抛出的错误放在e.error里,主体错误放在e.suppressed里。

如果多个资源在释放过程都抛出错误呢?

javascript 复制代码
function run() {
  function doSomething() {
    using a = funcA();
    using b = funcB();
    throw new Error('主体错误');
    // b[Symbol.dispose]() throw new Error('b错误')
    // a[Symbol.dispose]() throw new Error('a错误')
  }
  
  try {
    doSomething();
  } catch (e) {
    console.error(e);
  }
}

这时e的结构是:

javascript 复制代码
const e = {
  message: 'An error was suppressed during disposal.',
  name: 'SuppressedError',
  error: {
    message: 'a错误',
    name: 'Error',
    stack: '...',
  },
  suppressed: {
    message: 'An error was suppressed during disposal.',
    name: 'SuppressedError',
    error: {
      message: 'b错误',
      name: 'Error',
      stack: '...',
    },
    suppressed: {
      message: '主体错误',
      name: 'Error',
      stack: '...',
    },
    stack: '...',
  },
  stack: '...',
};

可以看到,主体代码抛出错误不会导致资源释放的代码不执行,资源b释放过程抛出错误也不会导致资源a得不到释放。

另外,错误会按照抛出的顺序使用suppressed串联起来(a => b => main),我们定义的错误可以通过e[0或多个.suppressed].error获得,最后一个错误通过e[0或多个.suppressed].suppressed获得。

await using声明

有时候,资源释放是异步的,这就需要使用await using来声明变量了,写法如下:

javascript 复制代码
await using file = doSomething();

await放在赋值操作左边?没错,第一次见这样的写法吧。它表明资源的释放是异步的,离开作用域时会调用await file[Symbol.asyncDispose]()或者await file[Symbol.dispose]()

注意,上面代码是否使用await using声明变量与doSomething是否异步函数没有任何关系。如果doSomething是异步函数,上面代码可以写成:

javascript 复制代码
await using file = await doSomething();

await using的实现可以用下面的代码表示:

javascript 复制代码
{
  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和using的实现很像,但也有一些差异。

不一定要有Symbol.asyncDispose

与using类似,await using声明的对象也有对应的方法用于资源释放:[Symbol.asyncDispose]()。如果这个方法找不到,会以[Symbol.dispose]()兜底。如果[Symbol.dispose]()也没有,则抛出TypeError

所以,无论是使用using还是await using,都定义[Symbol.dispose]()准没错。不过,对于await using来说,使用[Symbol.asyncDispose]()语义上更准确。

DisposableStack和AsyncDisposableStack

标准新增两个全局变量DisposableStackAsyncDisposableStack来统一管理资源的释放。

使用方式:

javascript 复制代码
function doSomething() {
  using stack = new DisposableStack();
  // stack是否已释放
  stack.disposed;
  // 相当于调用stack[Symbol.dispose]()
  stack.dispose();
  // 可以理解为数组的push方法
  const a = stack.use(funcA());
  const b = stack.use(funcB());
  // 如果不是标准的可释放资源(有Symbol.dispose方法),可以自定义dispose方法
  stack.adopt(funcC, () => {
    // dispose逻辑
  });
  // 往栈顶加入其他dispose逻辑,在stack的资源释放之前执行
  stack.defer(() => {
    // dispose逻辑
  });
  // 将stack里的资源移动到新的stack
  const newStack = stack.move();
}

AsyncDisposableStackDisposableStack的异步版本。

稍微提一下,使用DisposableStack时error的结构跟前面的不一样。

javascript 复制代码
function run() {
  function doSomething() {
    using stack = new DisposableStack();
    const a = stack.use(funcA());
    const b = stack.use(funcB());
    throw new Error('主体错误');
    // b[Symbol.dispose]() throw new Error('b错误')
    // a[Symbol.dispose]() throw new Error('a错误')
  }
  
  try {
    doSomething();
  } catch (e) {
    console.error(e);
  }
}

这里e的结构是:

javascript 复制代码
const e = {
  message: 'An error was suppressed during disposal.',
  name: 'SuppressedError',
  error: {
    message: '',
    name: 'SuppressedError',
    error: {
      message: 'a错误',
      name: 'Error',
      stack: '...',
    },
    suppressed: {
      message: 'b错误',
      name: 'Error',
      stack: '...',
    },
    stack: '...',
  },
  suppressed: {
    message: '主体错误',
    name: 'Error',
    stack: '...',
  },
  stack: '...',
};

也就是说代码主体抛出的错误放在e.suppressed,资源释放抛出的错误都放在e.error里,似乎把DisposableStack管理的资源抛出的错误作为一个整体了。

如何使用新特性

VSCode

目前为止,最新版的VSCode内置的ts版本还没达到5.2.0-beta,所以使用新特性会提示错误。 要想更改VSCode使用的ts版本,有两个便捷的方法。

第一个,如果本地安装了typescript 5.2 beta版本,可以将鼠标悬浮到状态栏TypeScript的位置,然后点击Select Version,或者Cmd+Shift+P快捷键唤起搜索框然后搜索TypeScript: Select TypeScript Version,选择使用workspace版本。 第二个,在User Settings里指定typescript.tsdk,可以使用本地或者全局安装的ts版本。

javascript 复制代码
{
  "typescript.tsdk": "/usr/local/lib/node_modules/typescript/lib"
}

tsc编译

要想tsc编译出来的js代码能使用新特性,需要修改tsconfig.jsontarget需要是es2022或以下,lib需要包含esnext或者esnext.disposable

json 复制代码
{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2022", "esnext.disposable", "dom"]
  }
}

另外,还需要对Symbol.dispose等进行polyfill,具体方式是引入下面的js:

javascript 复制代码
import "core-js/modules/esnext.symbol.dispose.js";
import "core-js/modules/esnext.symbol.async-dispose.js";
import "core-js/modules/esnext.disposable-stack.constructor.js";

参考资料

  1. devblogs.microsoft.com/typescript/...
  2. github.com/tc39/propos...
  3. code.visualstudio.com/docs/typesc...
  4. github.com/mizchi/play...
  5. github.com/microsoft/T...
相关推荐
y先森18 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy18 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891121 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
Jacky(易小天)3 小时前
MongoDB比较查询操作符中英对照表及实例详解
数据库·mongodb·typescript·比较操作符
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js