Typescript新特性: using 关键字

1,285 阅读4分钟

Typescript 的 using 关键字

ECMAScript 显式资源管理

本提案旨在解决软件开发中有关各种资源(内存、I/O 等)的生命周期和管理的常见模式。该模式通常包括资源的分配和明确释放关键资源的能力。

传送门:TC39:proposal-explicit-resource-management

怎么理解

想象一下:我们创建一个临时文件,对其进行读取和写入以进行各种操作,然后关闭并删除它。可能就有有以下代码:

import * as fs from "fs";
export function doSomeWork() {
    // open file
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");
    // doSomething
    ...
    // close file
    fs.closeSync(file);
    fs.unlinkSync(path);
}

某天需求变更,在某些条件下,有不同的处理逻辑

import * as fs from "fs";
export function doSomeWork() {
    // open file
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");

    if(someCondition()) {
        // do other something
        ...
        // close file
        fs.closeSync(file);
        fs.unlinkSync(path);
        return 
    }
    // do something
    ...
    // close file
    fs.closeSync(file);
    fs.unlinkSync(path);
}

可以看到一些重复的清理操作:

// close file
fs.closeSync(file);
fs.unlinkSync(path);

为了让代码更健壮,我们还得考虑异常边界,通常会使用try-catch捕获异常,并将清理操作放到finally代码块中:

function doSomeWork() {
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");
    try {
        // use file...
        if (someCondition()) {
            // do some more work...
            return;
        }
    }
    finally {
        // Close the file and delete it.
        fs.closeSync(file);
        fs.unlinkSync(path);
    }
}

从这个例子发现了什么: 如果业务越来越复杂,我么会在finally添加更多逻辑,”清理函数“会变得越来越臃肿,随着代码体积的变大,可能会出现其他的麻烦。

然后今天的主角登场:显式资源管理提案旨在提供一种标准化的方法来处理资源的获取和释放,使得 JavaScript 更容易处理资源泄漏问题。

using 关键字

作为JavaScript 的超集,在TypeScript中,可以使用using关键字来实现显式资源管理

Symbol.dispose

首先,在Symbol添加一个名为dispose的新内置函数Symbol.dispose,然后为对象创建一个[Symbol.dispose]方法。

class TempFile implements Disposable {  
    constructor() {
        
    }
    [Symbol.dispose]() {
        fs.closeSync(this.#handle);
        fs.unlinkSync(this.#path);
    }
}

清理操作就可以这么调用:

function doSomeWork() {
    const file = new TempFile(".some_temp_file");
    try {
        // ...
    }
    finally {
        file[Symbol.dispose]();
    }
}

但是这还远远不够,因为我们还是会显示去调用清理函数,对于开发者来说并没有减少太多工作量。

using声明

使用using来声明实例,它会创建固定绑定,使用using的变量会在scope末尾自动调用 Symbol.dispose 方法。

因此可以简化为以下代码。此就不用显示的去调用“清理函数”了。

export function doSomeWork() {
    using file = new TempFile(".some_temp_file");
    // use file...
    if (someCondition()) {
        // do some more work...
        return;
    }
}

using声明位置&执行顺序

1.using声明应该在作用域的return或throw之前;

2.多个using声明像堆栈一样,按照先进后出的顺序执行;

using声明异常处理

前文已经讲到:绑定的Symbol.dispose方法会在异常之前执行;但是以下情况又是怎么样的表现呢: 1:Symbol.dispose方式自身报错了会是怎样的表现呢? 2:Symbol.dispose和scope同时异常又会是怎么的表现呢?

针对scope和[Symbol.dispose]方法同时报错的情况,会抛出SuppressedError 类型的Error。SuppressedError类型如下:

interface SuppressedError extends Error {
    error: any; // [Symbol.dispose] 中的异常
    suppressed: any; // scope中的异常
}

async using

前文提到的都是同步方法,using关键字当然也是支持异步方法的。 1.声明Symbol.asyncDispose 异步方法; 2.使用await using声明变量;

class Logger {
    name: string;
    constructor(name: string) {
        this.name = name;
        console.log('created logger: ' + name);
    }
    [Symbol.asyncDispose] = async () => {
        await new Promise((resolve) =>
            setTimeout(() => {
                console.log('async disposed logger:', this.name);
                resolve(true);
            }, 1000)
        );
    };
}
async function doSomething() {   
    // 使用await using声明变量
    await using logger = new Logger('logger1');
    console.log('doing something executed!!!!');
 }

DisposableStack or AsyncDisposableStack

思考一下:上文讲述的“清理方法”是做好的写法吗?

DisposableStack和AsyncDisposableStack对于执行一次性清理以及任意数量的清理都很有用。

先看看DisposableStack的类型:

interface DisposableStack {
    /**
     * Returns a value indicating whether this stack has been disposed.
     */
    readonly disposed: boolean;
    /**
     * Disposes each resource in the stack in the reverse order that they were added.
     */
    dispose(): void;
    /**
     * Adds a disposable resource to the stack, returning the resource.
     */
    use<T extends Disposable | null | undefined>(value: T): T;
    /**
     * Adds a value and associated disposal callback as a resource to the stack.
     */
    adopt<T>(value: T, onDispose: (value: T) => void): T;
    /**
     * Adds a callback to be invoked when the stack is disposed.
     */
    defer(onDispose: () => void): void;
    /**
     * Move all resources out of this stack and into a new `DisposableStack`, and marks this stack as disposed.
     */
    move(): DisposableStack;
    [Symbol.dispose](): void;
    readonly [Symbol.toStringTag]: string;
}

可以看到,一个DisposableStack实例可以分别使用use,defer,adopt向堆栈中添加”清理方法”。调用实例的dispose方法后,按照先进后出的顺序执行堆栈中的“清理方法”。

class Logger {
    name: string;
    constructor(name: string) {
        this.name = name;
        console.log('created logger: ' + name);
    }

    [Symbol.dispose] = () => {
        console.log('disposed', this.name, '!!');
    };
}

function doSomething() {
    const stack = new DisposableStack();
    
    stack.defer(() => {
        console.log('disposed A !!');
    });
    stack.defer(() => {
        console.log('disposed B !!');
    });

    stack.adopt('disposed C !!', (value) => {
        console.log(value);
    });

    stack.use(new Logger('D'));

    console.log('doing something executed!!!!');
    stack.dispose();
}

doSomething();
//output:
//    created logger: D
//    doing something executed!!!!
//    disposed D !!
//    disposed C !!
//    disposed B !!
//    disposed A !!

using && DisposableStack

DisposableStack本身就是可处置的,怎么理解呢?可以使用using来声明DisposableStack实例!因此,不需要显示的调用dispose来执行堆栈中的清理函数,直接使用using来声明即可。

function doSomething() {
    using stack = new DisposableStack();   
    ...

    console.log('doing something executed!!!!');
}

doSomething();

怎么运行

1.安装polyfill disposablestack

2.设置tsconfig:

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