TypeScript – Using Disposable

前言

TypeScript v5.2 多了一个新功能叫 Disposable。

Dispose 的作用是让 "对象" 离开 "作用域" 后做出一些 "释放资源" 的操作。

很多地方都可以看到 Dispose 概念。比如 Web Component 的 disconnectedCallback,Angular 组件的 ngOnDestroy

而对象释放资源在其它面向对象语言中也很常见,比如 C# 的 Dispose Pattern

所以这一次 TypeScript 只是推出了一个本来就该有的功能而已。

之所以这么迟才推出,是因为这个功能可以用其它方式替代(虽然不够优雅),而且 Dispose 在后端比较需要,前端现在都迷函数式,对象反而越来越少见了。

 

参考

YouTube – NEW Way To Create Variables In JavaScript

Docs – Announcing TypeScript 5.2

TypeScript 5.2's New Keyword: 'using'

 

目前的支持度

Disposable 并不是 TypeSCript 语法,而是 JavaScript ECMA 标准。目前在 stage 3

当然 TypeScript 会做 down-level 处理,target ES2017 也可以使用,只是需要添加两句 runtime polyfill 就可以了。

在我写着一篇时,PrettierESLintesbuild 都还不支持 Disposable。要测试只能用 TypeScript Compiler (AKA tsc)。

Prettier Issue, esbuild issue 视乎还没人去提。

 

搭建环境

用 tsc 搭建环境,不会的可以看这篇

在 tsconfig.json 加入 compilerOptions.lib "ESNext.Disposable"

{
    "compilerOptions": {
        "target": "ES2017",
        "lib": ["ES2017", "ESNext.Disposable", "DOM"]
    }
}

我测试时完整的 tsconfig.json 如下

{
  "compilerOptions": {
    "target": "es2017",
    "lib": [
      "DOM",
      "DOM.Iterable",
      "ES2017",
      "ES2019.Array",
      "ES2019.Object",
      "ESNext.Disposable"
    ],
    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "useDefineForClassFields": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    /* Others */
    "skipLibCheck": true,
    "sourceMap": true,
  }
}
View Code

然后加上 runtime polyfill

(Symbol as { dispose: symbol }).dispose ??= Symbol("Symbol.dispose");
(Symbol as { asyncDispose: symbol }).asyncDispose ??= Symbol("Symbol.asyncDispose");

注: 我这里只是做了一个轻量版,严格来说需要添加更多的 polyfill 的。

详情参考这篇

 

没有 Disposable 怎么释放资源?

首先有个对象

class Person {
  private intervalNumber: number;
  constructor() {
    this.intervalNumber = setInterval(() => console.log('call'), 1000);
  }
  dispose() {
    clearInterval(this.intervalNumber);
  }
}

对象用到了一个全局资源  setInterval,并曝露了一个接口释放该资源。

使用的情况下是这样的。

document.addEventListener('click', () => {
  const person = new Person();
  // do something...
  person.dispose();
});

这样写不够安全,因为如果 do something 不小心报错,就会导致 dispose 没有被执行。于是

document.addEventListener('click', () => {
  const person = new Person();
  try {
    // do something...
  } catch {
    console.log('error');
  } finally {
    person.dispose();
  }
});

繁琐了很多,但是还可以接受,毕竟需要错误处理嘛。但是...如果我们不需要错误处理呢?

document.addEventListener('click', () => {
  const person = new Person();
  // do something...
  if (Math.random() > 0.5) return;
  person.dispose();
});

即便没有错误,只是 return 也会导致 dispose 没有执行。这个时候硬硬加 try finally 就有点过分了。

所以我们需要 Disposable 来优化,让代码变得优雅。

 

有 Disposable 怎样释放资源?

首先修改我们的 Person class。

class Person implements Disposable {
  private intervalNumber: number;
  
  constructor() {
    this.intervalNumber = setInterval(() => console.log('call'), 1000);
  }

  [Symbol.dispose] {
    clearInterval(this.intervalNumber);
  }
}

原本的 dispose 方法变成了 [Symbol.dispose] 方法。还有 class 多了一个 implements Disposable

对象属性名配 Symbol 这个 pattern 是 JavaScript 一路以来的玩法。Iterator 也是这个玩法。

使用的情况下是这样的。

document.addEventListener("click", () => {
  using person = new Person();
  // do something...
  if (Math.random() > 0.5) return;
});

person 前面把 const 改成了新的语法 "using"。然后...就没有然后了。

我们甚至都不需要调用 dispose。它自己会调用。

dispose 被调用的时机

using 在哪一个作用域,当那个作用域完结的时候,dispose 就会被调用。

上面例子中,我刻意加了一个花括弧作为 using 的 scope。所以触发的顺序是 1, 2, 3。

注意: JS 管理作用域的写法和 C# 不同哦。

下面是依据 C# 的写法。结果和上面的顺序是不相同的哦。不要搞错哦。

asyncDispose

如果释放资源是一个异步过程,那需要使用 asyncDispose

下面几个地方换成 async 就可以了。

然后调用时也加上 async 就可以了。

 

Downlevel Emit

参考: Github – Support using and await using declarations

using 会被 transpile 成 try finally。所以即使我们 target ES2017 也是可以使用 Displosable 功能的哦。

 

posted @ 2023-08-28 12:56  兴杰  阅读(352)  评论(0编辑  收藏  举报