MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript方法装饰器进阶:性能优化与日志记录

2024-01-186.0k 阅读

TypeScript方法装饰器的基础回顾

在深入探讨性能优化与日志记录之前,我们先来回顾一下TypeScript方法装饰器的基础知识。

方法装饰器是TypeScript中用于装饰类方法的一种特殊函数。它接收三个参数:目标对象(对于静态方法是类的构造函数,对于实例方法是类的原型对象)、方法名以及描述符对象。描述符对象包含了方法的一些属性,比如 value(方法的实际实现)、writable(是否可写)、enumerable(是否可枚举)和 configurable(是否可配置)。

以下是一个简单的方法装饰器示例:

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${propertyKey} with arguments:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} returned:`, result);
        return result;
    };
    return descriptor;
}

class MyClass {
    @log
    add(a: number, b: number) {
        return a + b;
    }
}

const myInstance = new MyClass();
myInstance.add(2, 3);

在这个示例中,log 装饰器在方法调用前后打印了日志信息。这展示了方法装饰器最基本的用途——对方法的行为进行增强。

利用方法装饰器进行性能优化

缓存计算结果

在一些场景下,某些方法的计算可能非常耗时,并且在相同的输入下,每次计算的结果都是相同的。这时,我们可以利用方法装饰器来缓存计算结果,从而提高性能。

function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cacheMap = new Map();
    descriptor.value = function (...args: any[]) {
        const key = args.toString();
        if (cacheMap.has(key)) {
            return cacheMap.get(key);
        }
        const result = originalMethod.apply(this, args);
        cacheMap.set(key, result);
        return result;
    };
    return descriptor;
}

class MathUtils {
    @cache
    complexCalculation(a: number, b: number) {
        // 模拟复杂计算
        let result = 0;
        for (let i = 0; i < 1000000; i++) {
            result += a * b + i;
        }
        return result;
    }
}

const mathUtils = new MathUtils();
console.time('first call');
console.log(mathUtils.complexCalculation(2, 3));
console.timeEnd('first call');
console.time('second call');
console.log(mathUtils.complexCalculation(2, 3));
console.timeEnd('second call');

在上述代码中,cache 装饰器使用 Map 来存储方法的输入和对应的输出。当方法被调用时,首先检查缓存中是否已经存在对应的结果,如果存在则直接返回,避免了重复的复杂计算。通过 console.timeconsole.timeEnd 我们可以看到,第二次调用相同参数的方法时,速度明显加快。

节流与防抖

节流(Throttle)和防抖(Debounce)是前端开发中常用的性能优化手段,方法装饰器也可以很方便地实现它们。

节流:限制函数在一定时间内只能被调用一次。

function throttle(delay: number) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        let lastCallTime = 0;
        descriptor.value = function (...args: any[]) {
            const now = new Date().getTime();
            if (now - lastCallTime >= delay) {
                lastCallTime = now;
                return originalMethod.apply(this, args);
            }
        };
        return descriptor;
    };
}

class ScrollHandler {
    @throttle(200)
    handleScroll() {
        console.log('Handling scroll event');
    }
}

window.addEventListener('scroll', () => {
    const scrollHandler = new ScrollHandler();
    scrollHandler.handleScroll();
});

在这个例子中,throttle 装饰器接收一个 delay 参数,表示函数调用的最小间隔时间。handleScroll 方法在滚动事件中被调用,但由于节流的作用,它在每 200 毫秒内最多只会被调用一次,从而减少了不必要的计算,提高了性能。

防抖:在一定时间内,如果函数被多次调用,只会执行最后一次调用。

function debounce(delay: number) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        let timer: NodeJS.Timeout | null = null;
        descriptor.value = function (...args: any[]) {
            if (timer) {
                clearTimeout(timer);
            }
            timer = setTimeout(() => {
                originalMethod.apply(this, args);
                timer = null;
            }, delay);
        };
        return descriptor;
    };
}

class SearchBox {
    @debounce(300)
    handleSearch(input: string) {
        console.log('Searching for:', input);
    }
}

const searchBox = new SearchBox();
const inputElement = document.createElement('input');
inputElement.addEventListener('input', (event) => {
    searchBox.handleSearch((event.target as HTMLInputElement).value);
});
document.body.appendChild(inputElement);

这里的 debounce 装饰器同样接收一个 delay 参数。当用户在输入框中输入内容时,handleSearch 方法并不会立即执行,而是在用户停止输入 300 毫秒后才会执行。如果在这 300 毫秒内用户又进行了新的输入,之前的定时器会被清除,重新开始计时,确保只执行最后一次输入后的搜索操作,避免了频繁的搜索请求。

方法装饰器用于日志记录的进阶技巧

详细的调用堆栈日志

在调试复杂应用时,了解方法的调用堆栈信息非常重要。我们可以通过方法装饰器来记录详细的调用堆栈。

function logCallStack(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const stack = new Error().stack;
        console.log(`Calling method ${propertyKey} from stack:`, stack);
        const result = originalMethod.apply(this, args);
        return result;
    };
    return descriptor;
}

class ComplexClass {
    @logCallStack
    innerMethod() {
        console.log('Inner method');
    }

    outerMethod() {
        this.innerMethod();
    }
}

const complexInstance = new ComplexClass();
complexInstance.outerMethod();

在上述代码中,logCallStack 装饰器利用 new Error().stack 获取当前的调用堆栈信息,并在方法调用时打印出来。这样,当 outerMethod 调用 innerMethod 时,我们可以清晰地看到方法调用的层级关系,有助于快速定位问题。

日志分类与级别

为了更好地管理日志,我们可以为日志添加分类和级别。

enum LogLevel {
    DEBUG = 'DEBUG',
    INFO = 'INFO',
    WARN = 'WARN',
    ERROR = 'ERROR'
}

function logger(category: string, level: LogLevel) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            const logMessage = `[${level}] [${category}] ${propertyKey} called with args: ${args}`;
            if (level === LogLevel.DEBUG) {
                console.debug(logMessage);
            } else if (level === LogLevel.INFO) {
                console.info(logMessage);
            } else if (level === LogLevel.WARN) {
                console.warn(logMessage);
            } else if (level === LogLevel.ERROR) {
                console.error(logMessage);
            }
            const result = originalMethod.apply(this, args);
            return result;
        };
        return descriptor;
    };
}

class UserService {
    @logger('User', LogLevel.INFO)
    createUser(username: string) {
        console.log('User created:', username);
    }

    @logger('User', LogLevel.ERROR)
    deleteUser(username: string) {
        console.log('User deletion failed for:', username);
    }
}

const userService = new UserService();
userService.createUser('John');
userService.deleteUser('Jane');

在这个示例中,logger 装饰器接收 categorylevel 两个参数。根据不同的日志级别,使用不同的 console 方法(console.debugconsole.infoconsole.warnconsole.error)来打印日志。这样我们可以方便地对日志进行分类和筛选,比如在开发环境中关注 DEBUG 级别的日志,而在生产环境中只关注 ERRORWARN 级别的日志。

异步方法的日志记录

在处理异步方法时,日志记录需要特殊处理,以确保能准确记录方法的执行过程。

function asyncLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: any[]) {
        console.log(`Starting async method ${propertyKey} with args:`, args);
        try {
            const result = await originalMethod.apply(this, args);
            console.log(`Async method ${propertyKey} completed successfully. Result:`, result);
            return result;
        } catch (error) {
            console.error(`Async method ${propertyKey} failed with error:`, error);
            throw error;
        }
    };
    return descriptor;
}

class ApiService {
    @asyncLogger
    async fetchData() {
        // 模拟异步操作
        return new Promise((resolve) => {
            setTimeout(() => {
                resolve('Data fetched');
            }, 1000);
        });
    }
}

const apiService = new ApiService();
apiService.fetchData().then(() => {});

asyncLogger 装饰器用于异步方法,在方法开始时打印日志,在方法成功或失败时也打印相应的日志。这样我们可以完整地跟踪异步方法的执行流程,方便调试和监控。

方法装饰器的组合使用

在实际应用中,我们经常需要同时使用多个装饰器来实现更复杂的功能。比如,我们可能既想缓存方法的计算结果,又想记录方法的调用日志。

function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cacheMap = new Map();
    descriptor.value = function (...args: any[]) {
        const key = args.toString();
        if (cacheMap.has(key)) {
            return cacheMap.get(key);
        }
        const result = originalMethod.apply(this, args);
        cacheMap.set(key, result);
        return result;
    };
    return descriptor;
}

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${propertyKey} with arguments:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} returned:`, result);
        return result;
    };
    return descriptor;
}

class CalculationService {
    @cache
    @log
    expensiveCalculation(a: number, b: number) {
        // 模拟昂贵的计算
        return a * b * a * b;
    }
}

const calculationService = new CalculationService();
calculationService.expensiveCalculation(2, 3);
calculationService.expensiveCalculation(2, 3);

在这个例子中,CalculationServiceexpensiveCalculation 方法同时应用了 cachelog 装饰器。首先,log 装饰器会在方法调用前后打印日志,然后 cache 装饰器会缓存计算结果,这样第二次调用相同参数的方法时,不仅可以看到日志,还能从缓存中快速获取结果,实现了性能优化与日志记录的双重目的。

需要注意的是,装饰器的应用顺序是从下往上的,即先应用 cache 装饰器,再应用 log 装饰器。在实际使用中,要根据具体需求合理安排装饰器的顺序,以达到预期的效果。

与依赖注入框架结合使用

在大型项目中,依赖注入框架(如InversifyJS)常用于管理对象之间的依赖关系。方法装饰器可以与依赖注入框架很好地结合,进一步增强应用的可维护性和可测试性。

假设我们使用InversifyJS来管理依赖,首先安装InversifyJS和reflect - metadata(TypeScript装饰器需要这个库):

npm install inversify reflect - metadata --save

然后定义一些接口和实现类:

import "reflect - metadata";
import { Container, injectable } from "inversify";

interface Logger {
    log(message: string): void;
}

@injectable()
class ConsoleLogger implements Logger {
    log(message: string) {
        console.log(message);
    }
}

interface DataService {
    fetchData(): string;
}

@injectable()
class MyDataService implements DataService {
    constructor(private logger: Logger) {}
    fetchData() {
        this.logger.log('Fetching data');
        return 'Some data';
    }
}

现在,我们可以使用方法装饰器来增强 DataService 的方法,例如添加缓存功能:

function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cacheMap = new Map();
    descriptor.value = function (...args: any[]) {
        const key = args.toString();
        if (cacheMap.has(key)) {
            return cacheMap.get(key);
        }
        const result = originalMethod.apply(this, args);
        cacheMap.set(key, result);
        return result;
    };
    return descriptor;
}

@injectable()
class MyDataService implements DataService {
    constructor(private logger: Logger) {}
    @cache
    fetchData() {
        this.logger.log('Fetching data');
        return 'Some data';
    }
}

最后,我们在应用中使用依赖注入框架来实例化对象:

const container = new Container();
container.bind<Logger>('Logger').to(ConsoleLogger);
container.bind<DataService>('DataService').to(MyDataService);

const dataService = container.get<DataService>('DataService');
console.log(dataService.fetchData());
console.log(dataService.fetchData());

通过这种方式,我们不仅利用依赖注入框架管理了对象的依赖关系,还通过方法装饰器对 DataService 的方法进行了性能优化。这种结合方式在大型项目中可以提高代码的模块化和可维护性,同时保持代码的清晰和简洁。

方法装饰器在不同运行环境中的注意事项

浏览器环境

在浏览器环境中,方法装饰器的使用需要注意兼容性。虽然现代浏览器大多支持ES6+的特性,但对于装饰器语法,可能需要通过Babel等工具进行转译。

在使用装饰器进行性能优化时,要注意缓存的清理。由于浏览器的内存限制,如果缓存占用过多内存,可能会导致性能问题。可以考虑设置缓存的过期时间或者在特定事件(如页面卸载)时清理缓存。

在日志记录方面,浏览器的 console 方法有不同的级别和样式支持。我们可以利用这些特性来使日志更加清晰和易于区分。例如,可以使用 console.groupconsole.groupEnd 方法来对相关的日志进行分组显示。

function logGroup(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.group(`Method ${propertyKey}`);
        console.log('Arguments:', args);
        const result = originalMethod.apply(this, args);
        console.log('Result:', result);
        console.groupEnd();
        return result;
    };
    return descriptor;
}

class BrowserUtils {
    @logGroup
    processData(data: any) {
        return data.toUpperCase();
    }
}

const browserUtils = new BrowserUtils();
browserUtils.processData('hello');

Node.js环境

在Node.js环境中,方法装饰器同样需要注意兼容性,特别是在低版本的Node.js中。通常也需要借助工具进行转译。

在性能优化方面,Node.js应用可能会面临多进程或多线程的场景。如果使用缓存,需要考虑缓存的一致性问题。例如,在集群模式下,不同进程之间的缓存同步可能需要额外的机制。

对于日志记录,Node.js有丰富的日志库,如 winstonpino。我们可以结合这些库来实现更强大的日志功能,如日志文件输出、日志分级管理等。

import winston from 'winston';

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transport.Console(),
        new winston.transport.File({ filename: 'app.log' })
    ]
});

function winstonLogger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        logger.info(`Calling method ${propertyKey} with args: ${args}`);
        const result = originalMethod.apply(this, args);
        logger.info(`Method ${propertyKey} returned: ${result}`);
        return result;
    };
    return descriptor;
}

class NodeService {
    @winstonLogger
    performTask() {
        return 'Task completed';
    }
}

const nodeService = new NodeService();
nodeService.performTask();

在这个例子中,我们使用 winston 库来记录日志,不仅在控制台输出,还写入到 app.log 文件中。通过方法装饰器,我们可以方便地为方法添加这种更强大的日志记录功能。

总结

通过上述内容,我们深入探讨了TypeScript方法装饰器在性能优化与日志记录方面的进阶应用。从基础的缓存、节流防抖,到详细的日志记录技巧,再到与依赖注入框架的结合以及在不同运行环境中的注意事项,方法装饰器展现出了强大的功能和灵活性。

在实际项目中,合理运用方法装饰器可以有效地提高代码的性能、可维护性和可测试性。希望这些知识和示例能帮助你在前端开发中更好地利用TypeScript方法装饰器,打造更健壮、高效的应用程序。