likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 16

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

背景

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

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

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

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以及全局声明之外的变量声明的方式。基本用法如下:

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声明的实现可以用下面的代码表示:

{
  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]()执行。

function run() {
  using a = funcA();
  using b = funcB();
  // b[Symbol.dispose]();
  // a[Symbol.dispose]();
}

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

错误处理

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

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的编译结果:

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

错误对象结构如下:

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里。

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

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的结构是:

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来声明变量了,写法如下:

await using file = doSomething();

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

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

await using file = await doSomething();

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

{
  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来统一管理资源的释放。

使用方式:

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的结构跟前面的不一样。

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的结构是:

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,所以使用新特性会提示错误。 Typescript 5.2 Beta发布,带来全新的using声明 要想更改VSCode使用的ts版本,有两个便捷的方法。

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

{
  "typescript.tsdk": "/usr/local/lib/node_modules/typescript/lib"
}

tsc编译

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

{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2022", "esnext.disposable", "dom"]
  }
}

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

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…
转载自:https://juejin.cn/post/7261549950554554423
评论
请登录