MST

星途 面试题库

面试题:TypeScript 装饰器实现日志记录的性能优化与高级特性

在高并发场景下,使用 TypeScript 装饰器实现日志记录可能会带来性能开销。请分析可能存在的性能问题,并提出至少两种优化方案。另外,如何利用装饰器元数据(Reflect Metadata)来进一步增强日志记录功能,比如根据特定的元数据信息决定是否记录日志等,给出具体实现思路和代码示例。
29.7万 热度难度
前端开发TypeScript

知识考点

AI 面试

面试题答案

一键面试

性能问题分析

  1. 函数调用开销:每次使用装饰器包装函数时,会额外增加函数调用的开销。因为装饰器本质上是一个函数,它会返回一个新的函数,这个新函数在每次调用时,除了执行原函数逻辑,还需要执行装饰器内部的逻辑,如日志记录逻辑。
  2. 元数据操作开销:如果在装饰器中使用 Reflect Metadata 来读取或写入元数据,这也会带来一定的性能开销。读取和写入元数据需要操作 JavaScript 的元数据存储机制,尤其是在高并发场景下,频繁的元数据操作可能会影响性能。

优化方案

  1. 缓存装饰器结果
    • 思路:对于相同的装饰器应用在不同函数上,如果装饰器逻辑不依赖于函数的具体实现(例如只记录函数名称和调用时间),可以缓存装饰器返回的新函数。这样在后续相同装饰器应用时,直接返回缓存的函数,避免重复创建新函数带来的开销。
    • 代码示例
const decoratorCache: Map<Function, Function> = new Map();

function logDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    if (decoratorCache.has(descriptor.value)) {
        return decoratorCache.get(descriptor.value);
    }
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling ${propertyKey} with args:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`${propertyKey} returned:`, result);
        return result;
    };
    decoratorCache.set(originalMethod, descriptor.value);
    return descriptor;
}
  1. 异步日志记录
    • 思路:将日志记录操作异步化,避免在函数调用路径中同步执行日志记录。可以使用 setTimeoutPromise 等方式将日志记录操作放到事件循环的下一个周期执行,从而减少对原函数调用性能的影响。
    • 代码示例
function asyncLogDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        const startTime = Date.now();
        const result = originalMethod.apply(this, args);
        setTimeout(() => {
            console.log(`Async log - Calling ${propertyKey} at ${startTime} with args:`, args);
            console.log(`${propertyKey} returned:`, result);
        }, 0);
        return result;
    };
    return descriptor;
}

利用装饰器元数据增强日志记录功能

  1. 具体实现思路
    • 使用 Reflect.defineMetadata 在装饰器中定义元数据,例如定义一个 shouldLog 的元数据来决定是否记录日志。
    • 在装饰器逻辑中,使用 Reflect.getMetadata 获取元数据,并根据元数据的值来决定是否执行日志记录操作。
  2. 代码示例
import 'reflect-metadata';

const SHOULD_LOG_KEY ='shouldLog';

function conditionalLogDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const shouldLog = Reflect.getMetadata(SHOULD_LOG_KEY, target, propertyKey);
    descriptor.value = function (...args: any[]) {
        if (shouldLog) {
            console.log(`Calling ${propertyKey} with args:`, args);
        }
        const result = originalMethod.apply(this, args);
        if (shouldLog) {
            console.log(`${propertyKey} returned:`, result);
        }
        return result;
    };
    return descriptor;
}

function setShouldLog(shouldLog: boolean) {
    return function (target: any, propertyKey: string) {
        Reflect.defineMetadata(SHOULD_LOG_KEY, shouldLog, target, propertyKey);
    };
}

class MyClass {
    @setShouldLog(true)
    @conditionalLogDecorator
    myMethod(a: number, b: number) {
        return a + b;
    }
}

const myObj = new MyClass();
myObj.myMethod(1, 2);