21、TypeScript 在服务器端的运行及异常、内存与性能管理

TypeScript 在服务器端的运行及异常、内存与性能管理

一、TypeScript 在服务器端的运行

JavaScript 在 Web 服务器领域并不陌生,借助 Node 和 Node 包管理器(NPM)提供的数千个模块,它获得了巨大的发展。随着在 Node 上运行的程序规模不断增大,TypeScript 提供的语言特性和工具的价值也迅速提升。因为在编写代码时,我们很容易犯一些简单的错误,比如将依赖异步调用的代码放在回调函数之外,或者混淆 string 和 String 的使用,而 TypeScript 可以帮助我们避免这些常见错误。

Express 框架是在 Node 上快速启动项目的好方法,对于有 Sinatra(或 .NET 中的 Nancy)使用经验的程序员来说会有一定的熟悉感。即使不熟悉这种实现风格,路由处理程序和视图的分离也很容易理解。与在 Node 中处理底层 HTTP 请求和响应相比,使用 Express 可以提高开发效率。

Mongoose 在数据库方面发挥着类似的作用,它提供了许多快捷方式,有助于提高开发效率。如果想要更底层地操作,直接调用 MongoDB 来存储和检索数据,处理模型和验证,MongoDB 也并不复杂。

虽然在很多情况下可以使用 Express 的默认设置,但我们并不局限于此,只需一行代码就能轻松替换模板引擎和中间件。

以下是一些关键要点:
- JavaScript 已经在 Web 服务器上运行了 20 多年。
- Node 可以在任何平台上运行。
- 可以从 NPM 的 @types 组织获取 Node 及许多 Node 模块的类型信息。
- Express 提供了一个轻量级且灵活的应用程序框架,比底层的 Node HTTP 请求和响应更容易使用。
- Mongoose 和 MongoDB 通过异步 API 提供简单的数据持久化功能。

二、异常处理

异常用于表明程序或模块无法继续处理。从本质上讲,异常应该只在真正特殊的情况下抛出。通常,异常用于指示程序状态无效或继续处理不安全。虽然在某些情况下,每次遇到不合意的参数就抛出异常可能很有吸引力,但对于可预见的输入,更优雅的做法是在不抛出异常的情况下进行处理。

当程序遇到异常时,如果没有在代码中进行处理,异常将显示在控制台中。在所有现代 Web 浏览器中,都可以检查控制台中的异常。不同浏览器和平台的快捷键可能不同,如果在 Windows 或 Linux 机器上 CTRL + SHIFT + I 不起作用,或者在 Mac 上 CMD + OPT + I 无效,可以尝试 F12 键,或者在浏览器菜单的“开发者工具”“浏览器控制台”或类似名称的选项中查找。对于 Node,错误和警告输出将显示在运行 HTTP 服务器的控制台窗口中。

1. 抛出异常

在 TypeScript 程序中,可以使用 throw 关键字抛出异常。虽然可以跟任何对象,但最好提供包含错误消息的字符串或包装错误消息的 Error 对象实例。

以下是一个典型的示例,用于防止不可接受的输入值:

function errorsOnThree(input: number) {
    if (input === 3) {
        throw new Error('Three is not allowed');
    }
    return input;
}
const result = errorsOnThree(3);

可以使用实现 Error 接口的类创建自定义异常。以下是一个自定义错误类的示例:

class ApplicationError implements Error {
    public name = 'ApplicationError';
    constructor(public message: string) {
        if (typeof console !== 'undefined') {
            console.log(`Creating ${this.name} "${message}"`);
        }
    }
    toString() {
        return `${this.name}: {this.message}`;
    }
}

可以使用自定义异常对发生的错误进行分类。常见的做法是创建一个通用的 ApplicationError 类,并从中继承以创建更具体的错误类型。例如:

class ApplicationError implements Error { 
    public name = 'ApplicationError';
    constructor(public message: string) {
        if (typeof console !== 'undefined') {
            console.log(`Creating ${this.name} "${message}"`);
        }
    }
    toString() {
        return `${this.name}: {this.message}`;
    }
}
class InputError extends ApplicationError {
}
function errorsOnThree(input: number) {
    if (input === 3) {
        throw new InputError('Three is not allowed');
    }
    return input;
}

需要注意的是,应将原生 Error 类型视为神圣不可侵犯的,永远不要抛出这种类型的异常。通过创建 ApplicationError 类的子类作为自定义异常,可以确保 Error 类型仅在真正特殊的情况下在代码外部使用。

2. 异常处理

当抛出异常时,除非进行处理,否则程序将终止。可以使用 try - catch 块、try - finally 块或 try - catch - finally 块来处理异常。在这些情况下,可能抛出异常的代码应放在 try 块中。

以下是一个 try - catch 块的示例,用于处理 errorsOnThree 函数抛出的错误:

try {
    const result = errorsOnThree(3);
} catch (err) {
    console.log('Error caught, no action taken');
}

catch 块中的 err 参数作用域仅限于该块,相当于使用 let 关键字声明的变量。

在支持 try - catch 块的语言中,通常允许捕获特定类型的异常。但目前在 TypeScript 中没有标准的条件捕获异常的方法,意味着要么捕获所有异常,要么不捕获。如果只想处理特定类型的异常,可以在 catch 语句中检查类型,并重新抛出不匹配的错误。

以下是一个检查错误类型的示例:

try {
    const result = errorsOnThree(3);
} catch (err) {
    if (err instanceof ApplicationError) {
        console.log('Error caught, no action taken');
    }
    throw err;
}

一般来说,在程序中处理的异常应该越具体越好。在接近底层代码时,应处理非常具体的异常类型;而在接近用户界面时,可以处理更通用的异常类型。

三、内存管理

在使用 TypeScript 或 JavaScript 等高级语言编写程序时,我们可以受益于自动内存管理。所有创建的变量和对象都会被自动管理,不会出现越界、悬空指针或变量损坏的问题。然而,有些内存安全问题无法自动处理,例如内存不足错误,这表明系统资源已耗尽,无法继续处理。

1. 释放资源

在 TypeScript 中,不太可能遇到未管理的资源。大多数 API 遵循异步模式,接受一个在操作完成时调用的方法参数,现代 API 通过 Promise 来实现。因此,程序通常不会直接持有未管理资源的引用。

例如,使用接近传感器的 API 时,可以这样写:

const sensorChange = function (reading) {
    const proximity = reading.near
       ? 'Near'
        : 'Far';
    alert(proximity);
}
window.addEventListener('userproximity', sensorChange, true);

但如果确实遇到需要管理的资源引用,应该使用 try - finally 块确保资源即使在发生错误时也能被释放。

以下是一个假设可以直接与接近传感器交互的示例:

const sensorChange = function (reading) {
    var proximity = reading.near ?
        'Near' : 'Far';
    alert(proximity);
}
const readProximity = function () {
    const sensor = new ProximitySensor();
    try {
        sensor.open();
        const reading = sensor.read();
        sensorChange(reading);
    } finally {
        sensor.close();
    }
}
window.setInterval(readProximity, 500);

对于类似 Promise 的接口,也有相应的处理方式:

const sensorChange = function (reading) {
    var proximity = reading.near ?
        'Near' : 'Far';
    alert(proximity);
}
const readProximity = function () {
    const sensor = new ProximitySensor();
    sensor.open()
       .then(() => {
            return sensor.read(); 
        })
       .then((reading) => {
            sensorChange(reading);
        })
       .finally(() => {
            sensor.close();
        });
}
window.setInterval(readProximity, 500);

可以将异常处理和资源管理结合起来,同时进行异常和内存管理。

2. 垃圾回收

当内存不再需要时,需要将其释放以便分配给程序中的其他对象。确定内存是否可以释放的过程称为垃圾回收。根据运行时环境的不同,会遇到几种不同的垃圾回收方式。

较旧的 Web 浏览器可能使用引用计数垃圾回收器,当对象的引用计数达到零时释放内存。这种方式速度很快,但如果两个或多个对象之间创建了循环引用,这些对象将永远不会被垃圾回收,因为它们的引用计数永远不会达到零。

现代 Web 浏览器使用标记 - 清除算法来解决这个问题,该算法会检测从根节点可达的所有对象,并对不可达的对象进行垃圾回收。虽然这种垃圾回收方式可能需要更长时间,但不太可能导致内存泄漏。为了防止浏览器 UI 冻结,一些 JavaScript 引擎会在空闲时间进行垃圾回收,减少对浏览体验的影响。

以下是引用计数垃圾回收的示例表格:
| 对象 | 引用计数 | 内存是否释放 |
| ---- | ---- | ---- |
| Object A | 1 | 否 |
| Object B | 1 | 否 |
| Object C | 1 | 否 |
| Object D | 1 | 否 |
| Object E | 0 | 是 |

大多数现代垃圾回收器会将对象提升到不同的代,对短期对象进行更频繁、更高效的回收。随着对象存活时间的增长,检查频率会降低,回收速度可能会变慢。完整的垃圾回收可能还包括一个压缩步骤,以优化内存使用。

使用标记 - 清除垃圾回收算法意味着在 TypeScript 程序中很少需要担心垃圾回收或内存泄漏问题。

四、性能优化

在编程领域,效率的追求有时会走向极端。程序员常常会花费大量时间去思考或担忧程序中非关键部分的速度,然而这些优化尝试在调试和维护时往往会产生负面影响。正如 Donald Knuth 所说:“我们应该在大约 97% 的时间里忘记小的效率问题,过早的优化是万恶之源。但在那关键的 3% 中,我们也不应错过机会。”

如果在没有可测量的性能问题之前就考虑性能优化,应该避免盲目进行。有很多文章声称使用局部变量比全局变量快,应避免使用闭包,因为它们速度慢,或者对象属性比变量慢。虽然这些说法通常是正确的,但将它们作为设计规则会导致程序设计不佳。

优化的黄金法则是,应该测量两个或多个潜在设计之间的差异,并决定性能提升是否值得为此做出的任何设计权衡。

在进行 TypeScript 程序的性能测量时,需要在多个平台上运行测试,否则可能在一个浏览器中速度变快,但在另一个浏览器中变慢。

下面通过一个简单的性能测试示例来详细说明。首先有一个轻量级的 CommunicationLines 类,它包含一个方法,使用著名的 n(n - 1)/2 算法计算团队成员之间的通信线路数量。 testCommunicationLines 函数用于实例化该类并测试团队规模为 4 和 10 的情况,它们分别有 6 和 45 条通信线路。

class CommunicationLines {
    calculate(teamSize: number) {
        return (teamSize * (teamSize - 1)) / 2
    }
}
function testCommunicationLines() {
    const communicationLines = new CommunicationLines();
    let result = communicationLines.calculate(4);
    if (result !== 6) {
        throw new Error('Test failed for team size of 4.');
    }
    result = communicationLines.calculate(10);
    if (result !== 45) {
        throw new Error('Test failed for team size of 10.');
    }
}
testCommunicationLines();

为了进行性能测试,使用 Performance 类。该类将回调函数包装在一个方法中,使用 performance.now 方法来计时操作。默认情况下, Performance 类会运行代码 10000 次,不过这个次数可以在调用 run 方法时进行覆盖。

export class Performance {
    constructor(private func: Function, private iterations: number) {
    }
    private runTest() {
        if (!performance) {
            throw new Error('The performance.now() standard is not supported in this runtime.');
        }
        const errors: number[] = [];
        const testStart = performance.now();
        for (let i = 0; i < this.iterations; i++) {
            try {
                this.func();
            } catch (err) {
                // Limit the number of errors logged
                if (errors.length < 10) {
                    errors.push(i);
                }
            }
        }
        const testTime = performance.now() - testStart;
        return {
            errors: errors,
            totalRunTime: testTime,
            iterationAverageTime: (testTime / this.iterations)
        };
    }
    static run(func: Function, iterations = 10000) {
        const tester = new Performance(func, iterations);
        return tester.runTest();
    }
}

要使用 Performance 类来测量程序,需要导入该类,并将 testCommunicationLines 函数传递给 Performance 类的 run 方法。

import { Performance } from './Listing-8-010';
class CommunicationLines {
    calculate(teamSize: number) {
        return (teamSize * (teamSize - 1)) / 2
    }
}
function testCommunicationLines() {
    const communicationLines = new CommunicationLines();
    let result = communicationLines.calculate(4);
    if (result !== 6) {
        throw new Error('Test failed for team size of 4.');
    }
    result = communicationLines.calculate(10);
    if (result !== 45) {
        throw new Error('Test failed for team size of 10.');
    }
}
const result = Performance.run(testCommunicationLines);
console.log(result.totalRunTime + ' ms');

运行这段代码后,控制台会记录总运行时间。在这个例子中,整个 10000 次迭代(即 20000 次调用通信线路算法)的运行时间不到 3 毫秒,这通常表明此时寻找优化机会可能找错了地方。

如果对代码进行调整,将团队规模为 4 的测试结果检查改为与 7 比较,测试将失败并抛出异常。

import { Performance } from './Listing-8-010';
class CommunicationLines {
    calculate(teamSize: number) {
        return (teamSize * (teamSize - 1)) / 2
    }
}
function testCommunicationLines() {
    const communicationLines = new CommunicationLines();
    let result = communicationLines.calculate(4);
    if (result !== 7) {
        throw new Error('Test failed for team size of 4.');
    }
    result = communicationLines.calculate(10);
    if (result !== 45) {
        throw new Error('Test failed for team size of 10.');
    }
}
const result = Performance.run(testCommunicationLines);
console.log(result.totalRunTime + ' ms');

运行带有失败测试和异常创建与处理的代码,总运行时间为 214.45 毫秒,比第一次测试慢了 78 倍。可以利用这些数据来指导设计决策,可能需要多次重复测试或尝试不同的迭代次数,以确保结果一致。

以下是使用 Performance 类进行的一些简单测试结果,以证明本节开头关于优化的说法。以每次迭代 0.74 毫秒的基线时间为例,结果如下(数值越高表示执行时间越慢):
| 情况 | 每次迭代时间(ms) | 比基线慢(ms) |
| ---- | ---- | ---- |
| 全局变量 | 0.80 | 0.06 |
| 闭包 | 1.13 | 0.39 |
| 属性 | 1.48 | 0.74 |

在 10000 次执行中,可以看到执行时间有小的差异。但需要记住,由于对象复杂度、嵌套深度、创建对象的数量等多种因素,程序的执行结果会有所不同。在进行任何优化之前,一定要进行测量,以便比较更改前后的性能,从而确定更改是否产生了积极影响。

综上所述,在 TypeScript 开发中,无论是服务器端的运行、异常处理、内存管理还是性能优化,都有各自的特点和最佳实践。我们应该根据实际情况,合理运用这些知识,以开发出高效、稳定的程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值