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 就可以了。
在我写着一篇时,Prettier、ESLint、esbuild 都还不支持 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, } }
然后加上 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 功能的哦。